feat: Implement initial with admin and mobile clients, authentication, data models, and lead generation scripts.

This commit is contained in:
2026-02-19 16:18:34 +01:00
parent c53a71a5f9
commit 5e2d5fb3ae
32 changed files with 2283 additions and 420 deletions

View File

Binary file not shown.

View File

@@ -1,12 +1,14 @@
// InnungsApp — Prisma Schema
// Stack: PostgreSQL + Prisma ORM + better-auth
// Stack: SQLite + Prisma ORM + better-auth
// Note: SQLite has no native enum support — enum fields are stored as String.
// Valid values are enforced at the application layer (Zod).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
provider = "sqlite"
url = env("DATABASE_URL")
}
@@ -87,16 +89,16 @@ model Verification {
// =============================================
model Organization {
id String @id @default(uuid())
id String @id @default(uuid())
name String
slug String @unique
plan Plan @default(pilot)
logoUrl String? @map("logo_url")
primaryColor String @default("#E63946") @map("primary_color")
contactEmail String? @map("contact_email")
avvAccepted Boolean @default(false) @map("avv_accepted")
slug String @unique
plan String @default("pilot") // pilot | standard | pro | verband
logoUrl String? @map("logo_url")
primaryColor String @default("#E63946") @map("primary_color")
contactEmail String? @map("contact_email")
avvAccepted Boolean @default(false) @map("avv_accepted")
avvAcceptedAt DateTime? @map("avv_accepted_at")
createdAt DateTime @default(now()) @map("created_at")
createdAt DateTime @default(now()) @map("created_at")
members Member[]
userRoles UserRole[]
@@ -107,62 +109,49 @@ model Organization {
@@map("organizations")
}
enum Plan {
pilot
standard
pro
verband
}
// =============================================
// MEMBERS
// =============================================
model Member {
id String @id @default(uuid())
orgId String @map("org_id")
userId String? @unique @map("user_id") // NULL until magic-link clicked
name String
betrieb String
sparte String
ort String
telefon String?
email String
status MemberStatus @default(aktiv)
istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb")
seit Int?
avatarUrl String? @map("avatar_url")
pushToken String? @map("push_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
id String @id @default(uuid())
orgId String @map("org_id")
userId String? @unique @map("user_id") // NULL until magic-link clicked
name String
betrieb String
sparte String
ort String
telefon String?
email String
status String @default("aktiv") // aktiv | ruhend | ausgetreten
istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb")
seit Int?
avatarUrl String? @map("avatar_url")
pushToken String? @map("push_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
newsAuthored News[] @relation("NewsAuthor")
stellen Stelle[]
terminAnmeldungen TerminAnmeldung[]
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
newsAuthored News[] @relation("NewsAuthor")
stellen Stelle[]
terminAnmeldungen TerminAnmeldung[]
@@index([orgId])
@@index([status])
@@map("members")
}
enum MemberStatus {
aktiv
ruhend
ausgetreten
}
// =============================================
// USER ROLES (multi-tenancy)
// =============================================
model UserRole {
id String @id @default(uuid())
orgId String @map("org_id")
userId String @map("user_id")
role OrgRole
createdAt DateTime @default(now()) @map("created_at")
id String @id @default(uuid())
orgId String @map("org_id")
userId String @map("user_id")
role String // admin | member
createdAt DateTime @default(now()) @map("created_at")
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -171,24 +160,19 @@ model UserRole {
@@map("user_roles")
}
enum OrgRole {
admin
member
}
// =============================================
// NEWS
// =============================================
model News {
id String @id @default(uuid())
orgId String @map("org_id")
authorId String? @map("author_id")
id String @id @default(uuid())
orgId String @map("org_id")
authorId String? @map("author_id")
title String
body String // Markdown
kategorie NewsKategorie
publishedAt DateTime? @map("published_at") // NULL = Entwurf
createdAt DateTime @default(now()) @map("created_at")
body String // Markdown
kategorie String // Wichtig | Pruefung | Foerderung | Veranstaltung | Allgemein
publishedAt DateTime? @map("published_at") // NULL = Entwurf
createdAt DateTime @default(now()) @map("created_at")
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
author Member? @relation("NewsAuthor", fields: [authorId], references: [id], onDelete: SetNull)
@@ -200,14 +184,6 @@ model News {
@@map("news")
}
enum NewsKategorie {
Wichtig
Pruefung
Foerderung
Veranstaltung
Allgemein
}
model NewsRead {
id String @id @default(uuid())
newsId String @map("news_id")
@@ -269,13 +245,13 @@ model Termin {
id String @id @default(uuid())
orgId String @map("org_id")
titel String
datum DateTime @db.Date
datum DateTime
uhrzeit String? // stored as "HH:MM"
endeDatum DateTime? @map("ende_datum") @db.Date
endeDatum DateTime? @map("ende_datum")
endeUhrzeit String? @map("ende_uhrzeit")
ort String?
adresse String?
typ TerminTyp
typ String // Pruefung | Versammlung | Kurs | Event | Sonstiges
beschreibung String?
maxTeilnehmer Int? @map("max_teilnehmer") // NULL = unbegrenzt
createdAt DateTime @default(now()) @map("created_at")
@@ -288,14 +264,6 @@ model Termin {
@@map("termine")
}
enum TerminTyp {
Pruefung
Versammlung
Kurs
Event
Sonstiges
}
model TerminAnmeldung {
id String @id @default(uuid())
terminId String @map("termin_id")

View File

@@ -2,6 +2,12 @@ import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// bcrypt-compatible hash using better-auth's default (sha256 fallback for seeding)
// better-auth uses its own hashing — we use the auth API to set a real password instead.
// For seeding we insert a known bcrypt hash for "demo1234".
// Generated with: https://bcrypt-generator.com/ (rounds=10)
const DEMO_PASSWORD_HASH = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lHny'
async function main() {
console.log('Seeding database...')
@@ -33,6 +39,19 @@ async function main() {
},
})
// Create password account so email+password login works in dev
await prisma.account.upsert({
where: { id: 'demo-admin-account-id' },
update: {},
create: {
id: 'demo-admin-account-id',
accountId: adminUser.id,
providerId: 'credential',
userId: adminUser.id,
password: DEMO_PASSWORD_HASH,
},
})
await prisma.userRole.upsert({
where: { orgId_userId: { orgId: org.id, userId: adminUser.id } },
update: {},