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>
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||
|
||
/** Rollen – bewusst nur zwei. */
|
||
export const rollen = ['ausbilder', 'azubi'] as const;
|
||
export type Rolle = (typeof rollen)[number];
|
||
|
||
export const benutzer = sqliteTable(
|
||
'benutzer',
|
||
{
|
||
id: text('id').primaryKey(),
|
||
name: text('name').notNull(),
|
||
email: text('email').notNull(),
|
||
passwortHash: text('passwort_hash').notNull(),
|
||
rolle: text('rolle', { enum: rollen }).notNull(),
|
||
aktiv: integer('aktiv').notNull().default(1),
|
||
erstelltAm: text('erstellt_am').notNull(),
|
||
letzteAnmeldungAm: text('letzte_anmeldung_am'),
|
||
},
|
||
(t) => [uniqueIndex('benutzer_email_idx').on(t.email)],
|
||
);
|
||
|
||
export const sitzung = sqliteTable(
|
||
'sitzung',
|
||
{
|
||
id: text('id').primaryKey(),
|
||
benutzerId: text('benutzer_id')
|
||
.notNull()
|
||
.references(() => benutzer.id, { onDelete: 'cascade' }),
|
||
erstelltAm: text('erstellt_am').notNull(),
|
||
laeuftAbAm: text('laeuft_ab_am').notNull(),
|
||
},
|
||
(t) => [index('sitzung_benutzer_idx').on(t.benutzerId), index('sitzung_ablauf_idx').on(t.laeuftAbAm)],
|
||
);
|
||
|
||
export const ausbildung = sqliteTable(
|
||
'ausbildung',
|
||
{
|
||
id: text('id').primaryKey(),
|
||
azubiId: text('azubi_id')
|
||
.notNull()
|
||
.references(() => benutzer.id, { onDelete: 'cascade' }),
|
||
ausbilderId: text('ausbilder_id')
|
||
.notNull()
|
||
.references(() => benutzer.id),
|
||
beruf: text('beruf').notNull().default('Fachinformatiker/-in für Systemintegration'),
|
||
beginn: text('beginn').notNull(),
|
||
ende: text('ende'),
|
||
probezeitBis: text('probezeit_bis'),
|
||
erstelltAm: text('erstellt_am').notNull(),
|
||
},
|
||
(t) => [uniqueIndex('ausbildung_azubi_idx').on(t.azubiId), index('ausbildung_ausbilder_idx').on(t.ausbilderId)],
|
||
);
|
||
|
||
export type Benutzer = typeof benutzer.$inferSelect;
|
||
export type Sitzung = typeof sitzung.$inferSelect;
|
||
export type Ausbildung = typeof ausbildung.$inferSelect;
|
||
|
||
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
|
||
export const alleTabellen = { benutzer, sitzung, ausbildung };
|