Baustein 1: Grundgeruest, Datenbank, Anmeldung, zwei Rollen
Legt das Fundament der Lern-Bar an: - Next.js 15 (App Router), React 19, TypeScript, ESLint, Vitest - SQLite ueber Drizzle hinter einer Repository-Schicht - Eigener Migrationslauf mit Pruefsummen, beim Start angewendet - Argon2id, serverseitige Sitzungen, HttpOnly-Cookie, SameSite=Lax - Rollen Ausbilder und Azubi, Rechteregeln als reine Funktionen - Verwaltung: Azubikonto und Ausbildung anlegen, Passwort, Sperren - Gestaltung Petrol/Bernstein, helle und dunkle Darstellung - Dockerfile, docker-compose, Lebenszeichen, update.sh mit Rueckfall - Sicherung und Wiederherstellung als Shell-Skripte - 29 Tests fuer Rechte, Zeitrechnung und Migrationslauf Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { datenbank, schema } from '../db';
|
||||
import { jetzt } from '../zeit';
|
||||
import type { Ausbildung, Benutzer } from '../db/schema';
|
||||
|
||||
export const STANDARD_BERUF = 'Fachinformatiker/-in für Systemintegration';
|
||||
|
||||
export function ausbildungNachAzubi(azubiId: string): Ausbildung | null {
|
||||
return datenbank().select().from(schema.ausbildung).where(eq(schema.ausbildung.azubiId, azubiId)).get() ?? null;
|
||||
}
|
||||
|
||||
export function ausbildungNachId(id: string): Ausbildung | null {
|
||||
return datenbank().select().from(schema.ausbildung).where(eq(schema.ausbildung.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
export type AusbildungMitAzubi = { ausbildung: Ausbildung; azubi: Benutzer };
|
||||
|
||||
export function ausbildungenVonAusbilder(ausbilderId: string): AusbildungMitAzubi[] {
|
||||
return datenbank()
|
||||
.select({ ausbildung: schema.ausbildung, azubi: schema.benutzer })
|
||||
.from(schema.ausbildung)
|
||||
.innerJoin(schema.benutzer, eq(schema.benutzer.id, schema.ausbildung.azubiId))
|
||||
.where(eq(schema.ausbildung.ausbilderId, ausbilderId))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function alleAusbildungen(): AusbildungMitAzubi[] {
|
||||
return datenbank()
|
||||
.select({ ausbildung: schema.ausbildung, azubi: schema.benutzer })
|
||||
.from(schema.ausbildung)
|
||||
.innerJoin(schema.benutzer, eq(schema.benutzer.id, schema.ausbildung.azubiId))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function ausbildungAnlegen(werte: {
|
||||
azubiId: string;
|
||||
ausbilderId: string;
|
||||
beruf?: string;
|
||||
beginn: string;
|
||||
ende?: string | null;
|
||||
probezeitBis?: string | null;
|
||||
}): Ausbildung {
|
||||
const datensatz: Ausbildung = {
|
||||
id: crypto.randomUUID(),
|
||||
azubiId: werte.azubiId,
|
||||
ausbilderId: werte.ausbilderId,
|
||||
beruf: werte.beruf?.trim() || STANDARD_BERUF,
|
||||
beginn: werte.beginn,
|
||||
ende: werte.ende ?? null,
|
||||
probezeitBis: werte.probezeitBis ?? null,
|
||||
erstelltAm: jetzt(),
|
||||
};
|
||||
datenbank().insert(schema.ausbildung).values(datensatz).run();
|
||||
return datensatz;
|
||||
}
|
||||
|
||||
export function ausbildungAendern(
|
||||
id: string,
|
||||
werte: Partial<Pick<Ausbildung, 'beruf' | 'beginn' | 'ende' | 'probezeitBis' | 'ausbilderId'>>,
|
||||
): void {
|
||||
datenbank().update(schema.ausbildung).set(werte).where(eq(schema.ausbildung.id, id)).run();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { asc, eq } from 'drizzle-orm';
|
||||
import { datenbank, schema } from '../db';
|
||||
import { passwortHashen } from '../auth/passwort';
|
||||
import { jetzt } from '../zeit';
|
||||
import type { Benutzer, Rolle } from '../db/schema';
|
||||
|
||||
/** Sämtlicher Zugriff auf die Tabelle `benutzer` läuft über dieses Modul. */
|
||||
|
||||
export function alleBenutzer(): Benutzer[] {
|
||||
return datenbank().select().from(schema.benutzer).orderBy(asc(schema.benutzer.name)).all();
|
||||
}
|
||||
|
||||
export function benutzerNachId(id: string): Benutzer | null {
|
||||
return datenbank().select().from(schema.benutzer).where(eq(schema.benutzer.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
export function benutzerNachEmail(email: string): Benutzer | null {
|
||||
return (
|
||||
datenbank()
|
||||
.select()
|
||||
.from(schema.benutzer)
|
||||
.where(eq(schema.benutzer.email, email.trim().toLowerCase()))
|
||||
.get() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function benutzerNachRolle(rolle: Rolle): Benutzer[] {
|
||||
return datenbank().select().from(schema.benutzer).where(eq(schema.benutzer.rolle, rolle)).all();
|
||||
}
|
||||
|
||||
export async function benutzerAnlegen(werte: {
|
||||
name: string;
|
||||
email: string;
|
||||
passwort: string;
|
||||
rolle: Rolle;
|
||||
}): Promise<Benutzer> {
|
||||
const id = crypto.randomUUID();
|
||||
const datensatz = {
|
||||
id,
|
||||
name: werte.name.trim(),
|
||||
email: werte.email.trim().toLowerCase(),
|
||||
passwortHash: await passwortHashen(werte.passwort),
|
||||
rolle: werte.rolle,
|
||||
aktiv: 1,
|
||||
erstelltAm: jetzt(),
|
||||
letzteAnmeldungAm: null,
|
||||
} satisfies Benutzer;
|
||||
|
||||
datenbank().insert(schema.benutzer).values(datensatz).run();
|
||||
return datensatz;
|
||||
}
|
||||
|
||||
export async function passwortSetzen(benutzerId: string, neuesPasswort: string): Promise<void> {
|
||||
const hash = await passwortHashen(neuesPasswort);
|
||||
datenbank().update(schema.benutzer).set({ passwortHash: hash }).where(eq(schema.benutzer.id, benutzerId)).run();
|
||||
// Alle bestehenden Sitzungen dieses Kontos verfallen.
|
||||
datenbank().delete(schema.sitzung).where(eq(schema.sitzung.benutzerId, benutzerId)).run();
|
||||
}
|
||||
|
||||
export function aktivSetzen(benutzerId: string, aktiv: boolean): void {
|
||||
datenbank()
|
||||
.update(schema.benutzer)
|
||||
.set({ aktiv: aktiv ? 1 : 0 })
|
||||
.where(eq(schema.benutzer.id, benutzerId))
|
||||
.run();
|
||||
if (!aktiv) {
|
||||
datenbank().delete(schema.sitzung).where(eq(schema.sitzung.benutzerId, benutzerId)).run();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user