0bfbcef043
- New 'accountant' role in companyRoleEnum (orthogonal like 'hr') - meetsMinRole and requireCompanyRole now exclude accountant from hierarchy along with hr - Settings UI exposes accountant in the role checkbox lists for both add-member and edit-member forms - New 'financial_exported' value added to companyLogEventEnum, ready for the upcoming export feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
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<App.Locals['user']> {
|
|
if (!locals.user) {
|
|
error(401, 'Authentication required');
|
|
}
|
|
return locals.user;
|
|
}
|
|
|
|
export function requireSystemAdmin(locals: App.Locals): NonNullable<App.Locals['user']> {
|
|
const user = requireAuth(locals);
|
|
if (!user.isSystemAdmin) {
|
|
error(403, 'System admin access required');
|
|
}
|
|
return user;
|
|
}
|
|
|
|
export async function getCompanyRoles(
|
|
userId: string,
|
|
companyId: string
|
|
): Promise<CompanyRole[] | null> {
|
|
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<CompanyRole, 'hr' | 'accountant'>
|
|
): boolean {
|
|
const minRank = ROLE_HIERARCHY[min];
|
|
for (const r of roles) {
|
|
if (r === 'hr' || r === 'accountant') continue;
|
|
const rank = ROLE_HIERARCHY[r as Exclude<CompanyRole, 'hr' | 'accountant'>];
|
|
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<CompanyRole, 'hr' | 'accountant'>
|
|
): Promise<{ user: NonNullable<App.Locals['user']>; 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<App.Locals['user']>; 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 };
|
|
}
|