Initial commit: Buildfor Life Budget app
Multi-company budget/project tracking tool built with SvelteKit 5, PostgreSQL (Drizzle ORM), and Tailwind CSS v4. Features: - Auth: local (email/password with Argon2) + generic OIDC - 4 roles per company: admin, manager, user, viewer - Multi-company with per-company user membership - Projects with budget allocation from company pool - Expense submission with approval workflow - Categories and tags for expense organization - Reports with spending breakdowns (by category, project, time) - CSV import for Actual Budget migration - Company audit log tracking all budget and admin actions - Remaining budget hero display on overview and budget pages - Admin-only company creation; new users wait for invitation - Deployment configs for systemd + nginx (bare metal/Proxmox) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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 getCompanyRole(
|
||||
userId: string,
|
||||
companyId: string
|
||||
): Promise<CompanyRole | null> {
|
||||
const result = await db
|
||||
.select({ role: companyMembers.role })
|
||||
.from(companyMembers)
|
||||
.where(and(eq(companyMembers.userId, userId), eq(companyMembers.companyId, companyId)))
|
||||
.limit(1);
|
||||
|
||||
if (result.length === 0) return null;
|
||||
return result[0].role;
|
||||
}
|
||||
|
||||
export async function requireCompanyRole(
|
||||
locals: App.Locals,
|
||||
companyId: string,
|
||||
minRole: CompanyRole
|
||||
): Promise<{ user: NonNullable<App.Locals['user']>; role: CompanyRole }> {
|
||||
const user = requireAuth(locals);
|
||||
|
||||
// System admins bypass company role checks
|
||||
if (user.isSystemAdmin) {
|
||||
return { user, role: 'admin' };
|
||||
}
|
||||
|
||||
const role = await getCompanyRole(user.id, companyId);
|
||||
|
||||
if (!role) {
|
||||
error(403, 'Not a member of this company');
|
||||
}
|
||||
|
||||
if (ROLE_HIERARCHY[role] < ROLE_HIERARCHY[minRole]) {
|
||||
error(403, `Requires ${minRole} role or higher`);
|
||||
}
|
||||
|
||||
return { user, role };
|
||||
}
|
||||
Reference in New Issue
Block a user