Initial import: live state on api.qbirr.com (server v0.6.3)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Multi-tenant teams (orgs). An org has an owner and members; the org's
|
||||
* subscription tier is inherited by all members (so a team owner pays
|
||||
* once and seats share the tier). Minimal v1: add existing users by
|
||||
* email; token-based email invites are a later refinement.
|
||||
*/
|
||||
|
||||
import { getDb } from '../db/pool.js';
|
||||
import { newId } from '../lib/crypto.js';
|
||||
import { getUserByEmail, getSubscription, type Tier } from './store.js';
|
||||
|
||||
export interface Org { id: string; name: string; owner_user_id: string; created_at: string; }
|
||||
export interface OrgMember { id: string; org_id: string; user_id: string; role: string; created_at: string; email?: string; }
|
||||
|
||||
export function createOrg(name: string, ownerUserId: string): Org {
|
||||
const id = newId();
|
||||
const db = getDb();
|
||||
db.prepare('INSERT INTO orgs (id, name, owner_user_id) VALUES (?,?,?)').run(id, name, ownerUserId);
|
||||
db.prepare('INSERT INTO org_members (id, org_id, user_id, role) VALUES (?,?,?,?)')
|
||||
.run(newId(), id, ownerUserId, 'owner');
|
||||
return getOrgById(id)!;
|
||||
}
|
||||
|
||||
export function getOrgById(id: string): Org | null {
|
||||
return (getDb().prepare('SELECT * FROM orgs WHERE id = ?').get(id) as Org | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function getOrgsForUser(userId: string): (Org & { role: string })[] {
|
||||
return getDb().prepare(`
|
||||
SELECT o.*, m.role FROM orgs o
|
||||
JOIN org_members m ON m.org_id = o.id
|
||||
WHERE m.user_id = ? ORDER BY o.created_at
|
||||
`).all(userId) as (Org & { role: string })[];
|
||||
}
|
||||
|
||||
export function isOwner(orgId: string, userId: string): boolean {
|
||||
const row = getDb().prepare('SELECT role FROM org_members WHERE org_id = ? AND user_id = ?')
|
||||
.get(orgId, userId) as { role: string } | undefined;
|
||||
return row?.role === 'owner';
|
||||
}
|
||||
|
||||
export function isMember(orgId: string, userId: string): boolean {
|
||||
return !!getDb().prepare('SELECT 1 FROM org_members WHERE org_id = ? AND user_id = ?').get(orgId, userId);
|
||||
}
|
||||
|
||||
export function getMembers(orgId: string): OrgMember[] {
|
||||
return getDb().prepare(`
|
||||
SELECT m.*, u.email FROM org_members m
|
||||
JOIN users u ON u.id = m.user_id
|
||||
WHERE m.org_id = ? ORDER BY m.created_at
|
||||
`).all(orgId) as OrgMember[];
|
||||
}
|
||||
|
||||
export function addMemberByEmail(orgId: string, email: string, role = 'member'): { ok: boolean; error?: string } {
|
||||
const user = getUserByEmail(email.trim().toLowerCase());
|
||||
if (!user) return { ok: false, error: 'user_not_found' }; // must have an account (invites: later)
|
||||
if (isMember(orgId, user.id)) return { ok: false, error: 'already_member' };
|
||||
getDb().prepare('INSERT INTO org_members (id, org_id, user_id, role) VALUES (?,?,?,?)')
|
||||
.run(newId(), orgId, user.id, role);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function removeMember(orgId: string, userId: string): void {
|
||||
// Never remove the owner via this path.
|
||||
getDb().prepare("DELETE FROM org_members WHERE org_id = ? AND user_id = ? AND role != 'owner'")
|
||||
.run(orgId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The best active tier a user inherits from any org they belong to
|
||||
* (via that org's owner's subscription). Returns null if none active.
|
||||
*/
|
||||
export function inheritedTier(userId: string): Tier | null {
|
||||
const orgs = getOrgsForUser(userId);
|
||||
let best: Tier | null = null;
|
||||
const rank: Record<Tier, number> = { basic: 1, pro: 2, max: 3 };
|
||||
for (const org of orgs) {
|
||||
const sub = getSubscription(org.owner_user_id);
|
||||
if (sub && (sub.status === 'active' || sub.status === 'trialing')) {
|
||||
if (!best || rank[sub.tier] > rank[best]) best = sub.tier;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
Reference in New Issue
Block a user