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:
2026-04-06 11:51:32 +07:00
commit 7a4ba0537f
86 changed files with 8963 additions and 0 deletions
+59
View File
@@ -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 };
}