import { ConvexError } from "convex/values"; import type { Id } from "./_generated/dataModel"; import type { MutationCtx, QueryCtx } from "./_generated/server"; export interface AuthContext { readonly auth: { readonly getUserIdentity: () => Promise<{ readonly tokenIdentifier: string; } | null>; }; } export const requireAuthUserId = async (ctx: AuthContext): Promise => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new ConvexError("Authentication required"); } return identity.tokenIdentifier; }; /** * Resolve the authenticated identity and prove it is a member of the given * organization. Throws on unauthenticated or non-member access. Returns the * canonical user ID (the Better Auth `tokenIdentifier`). */ export const requireOrganizationMember = async ( ctx: QueryCtx | MutationCtx, organizationId: Id<"organizations"> ): Promise => { const userId = await requireAuthUserId(ctx); const membership = await ctx.db .query("organizationMembers") .withIndex("by_organizationId_and_userId", (q) => q.eq("organizationId", organizationId).eq("userId", userId) ) .unique(); if (!membership) { throw new ConvexError("Organization membership required"); } return userId; }; /** * Resolve the authenticated user's personal/current organization. Browsers * never submit organization IDs for Project commands; this resolves the * tenant boundary server-side through the authenticated identity. */ export const requireCurrentOrganization = async ( ctx: QueryCtx | MutationCtx ): Promise<{ organizationId: Id<"organizations">; userId: string }> => { const userId = await requireAuthUserId(ctx); const organization = await ctx.db .query("organizations") .withIndex("by_createdBy_and_kind", (q) => q.eq("createdBy", userId).eq("kind", "personal") ) .unique(); if (!organization) { throw new ConvexError("Organization not found"); } return { organizationId: organization._id, userId }; }; /** * Prove the authenticated user is a member of the project's organization. * Projects are organization-scoped; unauthorized single-object operations * throw "Project not found" to avoid leaking existence. */ export const requireProjectMember = async ( ctx: QueryCtx | MutationCtx, projectId: Id<"projects"> ): Promise<{ organizationId: Id<"organizations">; userId: string }> => { const project = await ctx.db.get(projectId); if (!project) { throw new ConvexError("Project not found"); } const userId = await requireOrganizationMember(ctx, project.organizationId); return { organizationId: project.organizationId, userId }; };