import { error } from '@sveltejs/kit'; import { db } from './db/index.js'; import { companyMembers } from './db/schema.js'; import { and, eq } from 'drizzle-orm'; import { ROLE_HIERARCHY, type CompanyRole } from '$lib/types/index.js'; export function requireAuth(locals: App.Locals): NonNullable { if (!locals.user) { error(401, 'Authentication required'); } return locals.user; } export function requireSystemAdmin(locals: App.Locals): NonNullable { const user = requireAuth(locals); if (!user.isSystemAdmin) { error(403, 'System admin access required'); } return user; } export async function getCompanyRoles( userId: string, companyId: string ): Promise { const result = await db .select({ roles: companyMembers.roles }) .from(companyMembers) .where(and(eq(companyMembers.userId, userId), eq(companyMembers.companyId, companyId))) .limit(1); if (result.length === 0) return null; return result[0].roles as CompanyRole[]; } /** Does the role set include the target role? */ export function hasRole(roles: CompanyRole[], target: CompanyRole): boolean { return roles.includes(target); } /** Does any hierarchical role in the set meet or exceed the minimum rank? `hr` and `accountant` do not count. */ export function meetsMinRole( roles: CompanyRole[], min: Exclude ): boolean { const minRank = ROLE_HIERARCHY[min]; for (const r of roles) { if (r === 'hr' || r === 'accountant') continue; const rank = ROLE_HIERARCHY[r as Exclude]; if (rank >= minRank) return true; } return false; } /** * Ensure the caller meets the minimum hierarchical role (admin>manager>user>viewer). * System admins bypass. Returns the caller's full role set. */ export async function requireCompanyRole( locals: App.Locals, companyId: string, minRole: Exclude ): Promise<{ user: NonNullable; roles: CompanyRole[] }> { const user = requireAuth(locals); if (user.isSystemAdmin) { return { user, roles: ['admin'] }; } const roles = await getCompanyRoles(user.id, companyId); if (!roles || roles.length === 0) { error(403, 'Not a member of this company'); } if (!meetsMinRole(roles, minRole)) { error(403, `Requires ${minRole} role or higher`); } return { user, roles }; } /** * Ensure the caller has ANY of the listed roles. Useful for orthogonal roles like `hr`. * System admins bypass. */ export async function requireCompanyRoleAny( locals: App.Locals, companyId: string, anyOf: CompanyRole[] ): Promise<{ user: NonNullable; roles: CompanyRole[] }> { const user = requireAuth(locals); if (user.isSystemAdmin) { return { user, roles: ['admin'] }; } const roles = await getCompanyRoles(user.id, companyId); if (!roles || roles.length === 0) { error(403, 'Not a member of this company'); } const has = roles.some((r) => anyOf.includes(r)); if (!has) { error(403, `Requires one of: ${anyOf.join(', ')}`); } return { user, roles }; }