Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import type { Database } from "@app/database";
import { schema } from "@app/database";
import type { EventType } from "@app/shared-types";
import type { NewDomainEvent } from "@app/events";
import { errors } from "./errors.js";
/** Slumpad, läsbar inbjudningskod utan lättförväxlade tecken. */
export function generateInviteCode(): string {
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const bytes = randomBytes(8);
let code = "";
for (const b of bytes) code += alphabet[b % alphabet.length];
return code;
}
export function sha256(input: string): string {
return createHash("sha256").update(input).digest("hex");
}
export function todayIso(): string {
return new Date().toISOString().slice(0, 10);
}
export function currentMonth(): string {
return new Date().toISOString().slice(0, 7);
}
/** Verifiera hushållsmedlemskap enda vägen till hushållsdata (spec §56). */
export async function requireMembership(
db: Database,
householdId: string,
userId: string,
): Promise<{ role: string; portionFactor: number }> {
const [member] = await db
.select()
.from(schema.householdMembers)
.where(
and(
eq(schema.householdMembers.householdId, householdId),
eq(schema.householdMembers.userId, userId),
),
)
.limit(1);
if (!member) throw errors.forbidden("Du är inte medlem i det här hushållet.");
return { role: member.role, portionFactor: member.portionFactor };
}
/** Hämta användarens aktiva hushåll (första medlemskapet) eller null. */
export async function getActiveHouseholdId(db: Database, userId: string): Promise<string | null> {
const [member] = await db
.select({ householdId: schema.householdMembers.householdId })
.from(schema.householdMembers)
.where(eq(schema.householdMembers.userId, userId))
.orderBy(schema.householdMembers.joinedAt)
.limit(1);
return member?.householdId ?? null;
}
/** Som ovan men kastar 404 om användaren saknar hushåll. */
export async function requireActiveHousehold(db: Database, userId: string): Promise<string> {
const id = await getActiveHouseholdId(db, userId);
if (!id) {
throw errors.notFound("Du har inget hushåll ännu. Skapa ett under Hemma → Hushåll.");
}
return id;
}
/** Skriv domänhändelse till outboxen (spec §55) i samma transaktion när tx skickas in. */
export async function emitEvent<T extends EventType>(
db: Database,
event: NewDomainEvent<T>,
): Promise<void> {
await db.insert(schema.domainEvents).values({
type: event.type,
userId: event.userId ?? null,
householdId: event.householdId ?? null,
payload: event.payload as Record<string, unknown>,
correlationId: event.correlationId ?? null,
});
}
/** Audit-logg (spec §56). */
export async function audit(
db: Database,
entry: {
actorUserId?: string | undefined;
actorType?: "user" | "admin" | "system" | "worker";
action: string;
targetType?: string;
targetId?: string;
metadata?: Record<string, unknown>;
ip?: string | undefined;
correlationId?: string | undefined;
},
): Promise<void> {
await db.insert(schema.auditLogs).values({
actorUserId: entry.actorUserId ?? null,
actorType: entry.actorType ?? "user",
action: entry.action,
targetType: entry.targetType ?? null,
targetId: entry.targetId ?? null,
metadata: entry.metadata ?? null,
ip: entry.ip ?? null,
correlationId: entry.correlationId ?? null,
});
}
export function newCorrelationId(): string {
return randomUUID();
}