Files
zpan/server/services/org.ts
T
Jasper VanandBob a22448cbc4 feat: add team RBAC middleware for org-level role enforcement (#293)
Introduces requireTeamRole middleware that enforces viewer/editor/owner
role hierarchy on object and trash routes. Personal orgs bypass the
check; non-members receive 403. Adds getMemberRole and isPersonalOrg
helpers to org service. Adds 'member' default-role mapping at viewer
level to prevent silent lockout of users created by better-auth.

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-14 03:34:19 -04:00

44 lines
1.8 KiB
TypeScript

import { and, eq } from 'drizzle-orm'
import { member, organization } from '../db/auth-schema'
import type { Database } from '../platform/interface'
// Find the user's personal org, if they still belong to it. The personal org
// slug is a deterministic `personal-${user.id}` written by createAuth's
// user.create.after hook, so we filter on the indexed UNIQUE slug column and
// then verify the member row still exists. Verifying membership is load-
// bearing: an admin can revoke a user's access by deleting the member row
// without deleting the org, and the caller must treat that user as orphaned.
export async function findPersonalOrg(db: Database, userId: string): Promise<string | null> {
const rows = await db
.select({ orgId: organization.id })
.from(organization)
.innerJoin(member, and(eq(member.organizationId, organization.id), eq(member.userId, userId)))
.where(eq(organization.slug, `personal-${userId}`))
.limit(1)
return rows[0]?.orgId ?? null
}
// Return the user's role in the given org, or null if they are not a member.
export async function getMemberRole(db: Database, orgId: string, userId: string): Promise<string | null> {
const rows = await db
.select({ role: member.role })
.from(member)
.where(and(eq(member.organizationId, orgId), eq(member.userId, userId)))
.limit(1)
return rows[0]?.role ?? null
}
// Personal orgs use a deterministic slug `personal-${userId}`. Checking the
// slug is sufficient — no additional query is needed.
export async function isPersonalOrg(db: Database, orgId: string): Promise<boolean> {
const rows = await db
.select({ slug: organization.slug })
.from(organization)
.where(eq(organization.id, orgId))
.limit(1)
return (rows[0]?.slug ?? '').startsWith('personal-')
}