Gå til indholdet

Arkitektur

Levende dokument — opdateres i samme MR som arkitekturændringer (jf. CLAUDE.md). Senest opdateret ved indførelsen af forløbsmodellen (ADR-001).

Overblik

Fuld-stack TypeScript-monorepo: backend/ (Express + Bun + Prisma + PostgreSQL) og frontend/ (React 18 + Vite + MUI + Redux Toolkit). Realtid via Socket.io; dokumentsamarbejde via OnlyOffice Document Server; auth via Better Auth med Organization-plugin (multi-tenancy).

Browser ── frontend (nginx) ──┬── /api          → backend:3000
                              ├── /api/socket.io → backend (websocket)
                              └── /ds-vpath/     → documentserver
backend ── Prisma ── PostgreSQL
backend ⇄ documentserver (intern JWT: ONLYOFFICE_JWT_SECRET)

Frontend-containeren er eneste offentlige ingress; SSL/routing håndteres af platform-proxyen (Coolify i produktion, Traefik via compose.ci.yml på build-serveren). Fail-fast env-validering ved boot i config/env.ts.

Backend-lagdeling

routes/controllers/services/ → Prisma. Middleware-kæde: requestIdrequestLoggerauthenticate (Better Auth-session fra cookie) → resolveTenant (sætter req.organizationId) → authorize(roller)validate(zod) → handler → errorHandler.

Audit-spor: logging.service.ts skriver ActivityLog fra alle centrale mutationer (klasse/forløb/gruppe/opgave, inkl. BULK_UPDATE) — eksponeret rolle-scopet via GET /api/activity-logs.

Domænemodel

Begreberne fra mødenoterne og deres modeller (ADR-001):

Begreb Model Rolle
Hold Class Persongruppen: elever (ClassEnrollment), skema, fag (ClassSubject), lærer
Fag Subject Organisationens taksonomi ("Matematik")
Forløb Course Pædagogisk enhed ("Geometri") for ét hold + ét fag; unik på (classId, subjectId, name)
Emne Assignment Én opgaverunde i et forløb: title = emne-navn ("Figurer"), description = opgavetekst, deadline = eneste frist-kilde, isShared = delt/unik opgavetekst
Gruppe Group Elevgruppe i et hold; courseId placerer den i forløbet; startDate styrer elevsynlighed
Aflevering GroupAssignment Forretningsreglen "én gruppe = én aflevering": unik pr. (assignment, group); bærer status, submissions, descriptionOverride (unik opgavetekst)
Dokument WorkRoomDocument OnlyOffice-dokument i gruppens arbejdsrum (WorkRoom, 1:1 med gruppe)
Organization ─ Member (org-roller) ─ User
  └─ Class (HOLD) ── ClassSubject ─> Subject (FAG)
       ├─ Course (FORLØB, ADR-001)  ←──────────────┐
       │     ├─ Assignment (EMNE + opgavetekst)    │ courseId-invariant:
       │     └─ Group.courseId ────────────────────┘ gruppe følger sin
       ├─ Assignment ── GroupAssignment ─> Group     aktive opgaves forløb
       │     │            ├─ descriptionOverride   (unik tekst pr. gruppe)
       │     │            ├─ Submission(s) + currentSubmission
       │     │            └─ AssignmentAttachment.groupAssignmentId
       │     │               (null = delt fil, sat = kun den gruppe)
       │     └─ isShared (delt/unik-toggle)
       └─ Group ── GroupMember ─> User
             ├─ startDate  (fremtidig ⇒ usynlig for elever, inkl. sockets)
             └─ WorkRoom (1:1) ── WorkRoomDocument ── WorkRoomMessage

Effektiv opgavetekst og filer (ADR-001)

Én regel, ét sted (AssignmentService.effectiveDescription):

effectiveDescription = isShared ? assignment.description
                                : (ga.descriptionOverride ?? assignment.description)

Alle assignment-svar beriges med effectiveDescription pr. groupAssignment. Filer: elever ser emnets delte filer (groupAssignmentId IS NULL) plus deres egen gruppes — håndhævet i listAttachments.

Bulk-redigering ("alle grupper med dette emne")

PATCH /api/assignments/:id/bulk opdaterer KUN delte felter (titel/beskrivelse/frist + gruppernes startDate) — aldrig medlemmer, overrides eller pr.-gruppe-filer. ?dryRun=1 returnerer preview af berørte grupper ("Dette ændrer N grupper i …"). Anvendelser logges som ASSIGNMENT BULK_UPDATE i ActivityLog med berørte gruppe-id'er.

Kontrakt-migration (gennemført)

Group.deadline og Group.topic er droppet (20260611150000_drop_group_deadline_and_topic): frist læses fra assignment.deadline via GroupAssignment, og emnet bæres af Assignment.title + Course. Datarapport ved migrering: 0 grupper havde deadline uden tilknyttet opgave — ingen frist-information gik tabt.

Auth & multi-tenancy

Better Auth (cookie-sessions, 24t/1t refresh/5 min cookie-cache) + Organization-plugin. Platform-roller User.role ∈ {SUPER_ADMIN, ADMIN, TEACHER, STUDENT}; org-roller mappes til app-roller i authenticate. Org-scoping i alle queries via class.organizationId-kæden. Se ADR 0001-fjern-legacy-jwt.

OnlyOffice

workroom.controller.ts: editor-config signeres med ONLYOFFICE_JWT_SECRET; dokument-download/callback er offentlige endpoints beskyttet ALENE af samme JWT (påkrævet ved boot). Dokumentnøgle document_{id}_{mtime} er stabil på tværs af brugere (live-samarbejde).

Realtid (Socket.io)

Rum: user:<id>, class:<id>, class:<id>:staff (kun lærere/admins — bruges til grupper med fremtidig startDate), group:<id>. Elever joiner ikke gruppe-rum for fremtidigt planlagte grupper.

Test og drift

  • 70+ integrationstests (bun run test) med Better Auth-helper (src/test/helpers/auth.ts); CI blokerer på testfejl.
  • Migrationer: expand/contract; destruktive ændringer kun i separate kontrakt-migrationer, og alt testes mod kopi af produktionsdata før merge. Reset af database sker KUN via backend/scripts/reset-db.sh.
  • Se også docs/operations.md (miljøvariabler, seed, reset) og docs/funktioner.md.