ada1b51f33
- docs/api.md: komplette API-Referenz (1375 Zeilen, alle Endpunkte) - docs/architecture.md: Tech-Stack, DB-Schema, RLS-Architektur, Auth-Flow - docs/deployment.md: Setup, nginx, systemd, update.sh, Backup/Rollback - docs/development.md: Dev-Setup, Test-Workflow, Code-Konventionen, Fallstricke Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
462 lines
19 KiB
Markdown
462 lines
19 KiB
Markdown
# TimeMaster – Architektur-Dokumentation
|
||
|
||
Stand: 2026-05-24
|
||
|
||
---
|
||
|
||
## Tech-Stack
|
||
|
||
| Schicht | Technologie | Version |
|
||
|---------|-------------|---------|
|
||
| Backend | Python / FastAPI | 3.12 / 0.115+ |
|
||
| ORM | SQLAlchemy async | 2.x |
|
||
| Datenbank | PostgreSQL | 16 |
|
||
| Cache / Sessions | Redis | 7 |
|
||
| Frontend | React + TypeScript | 18 / 5 |
|
||
| CSS-Framework | Tailwind CSS | 3 |
|
||
| Build-Tool | Vite | 5 |
|
||
| Prozess-Manager | systemd | – |
|
||
| Reverse Proxy | nginx | 1.24+ |
|
||
| Migrationen | Alembic | 1.x |
|
||
| Authentifizierung | JWT (python-jose) + bcrypt | – |
|
||
| 2FA | pyotp (TOTP / RFC 6238) | – |
|
||
| E-Mail | Resend.com HTTP API | – |
|
||
| Tests | pytest + pytest-asyncio + httpx | – |
|
||
|
||
---
|
||
|
||
## Verzeichnisstruktur
|
||
|
||
```
|
||
/opt/timemaster/
|
||
├── backend/
|
||
│ ├── app/
|
||
│ │ ├── core/
|
||
│ │ │ ├── config.py # Pydantic Settings (liest .env)
|
||
│ │ │ ├── database.py # Engine, AsyncSessionLocal, Base
|
||
│ │ │ ├── dependencies.py # FastAPI Depends: get_current_user, require_role
|
||
│ │ │ ├── limiter.py # slowapi Rate-Limiter
|
||
│ │ │ └── security.py # JWT, Passwort-Hash, Token-Utilities
|
||
│ │ ├── models/
|
||
│ │ │ ├── company.py # Company, Department
|
||
│ │ │ ├── user.py # User, UserRole, AuthProvider
|
||
│ │ │ ├── session.py # Refresh-Token-Sessions
|
||
│ │ │ ├── password_reset.py # Passwort-Reset-Tokens
|
||
│ │ │ ├── audit_log.py # AuditLog
|
||
│ │ │ ├── absence.py # Absence, AbsenceStatus
|
||
│ │ │ ├── absence_type.py # AbsenceType
|
||
│ │ │ ├── vacation_balance.py
|
||
│ │ │ ├── public_holiday.py # Feiertage
|
||
│ │ │ ├── overtime_balance.py
|
||
│ │ │ ├── time_entry.py # TimeEntry, WorkSchedule
|
||
│ │ │ ├── work_schedule.py
|
||
│ │ │ ├── kiosk_device.py
|
||
│ │ │ ├── caldav_config.py # CalDAV-Konfig (Firma + User)
|
||
│ │ │ ├── ldap_config.py
|
||
│ │ │ ├── smtp_config.py
|
||
│ │ │ └── project.py # (derzeit nicht aktiv, Migration 0014 entfernt)
|
||
│ │ ├── schemas/ # Pydantic v2 Ein-/Ausgabe-Schemas
|
||
│ │ │ ├── auth.py
|
||
│ │ │ ├── user.py
|
||
│ │ │ ├── absence.py
|
||
│ │ │ ├── time_entry.py
|
||
│ │ │ └── ...
|
||
│ │ ├── routers/ # FastAPI APIRouter – ein File pro Modul
|
||
│ │ │ ├── auth.py # /api/v1/auth/*
|
||
│ │ │ ├── users.py # /api/v1/users/*
|
||
│ │ │ ├── companies.py # /api/v1/companies/*
|
||
│ │ │ ├── absences.py # /api/v1/absences/*
|
||
│ │ │ ├── time_entries.py # /api/v1/time/*
|
||
│ │ │ ├── kiosk.py # /api/v1/kiosk/*
|
||
│ │ │ ├── reports.py # /api/v1/reports/*
|
||
│ │ │ ├── audit.py # /api/v1/audit-logs/
|
||
│ │ │ ├── caldav.py
|
||
│ │ │ ├── ldap.py
|
||
│ │ │ ├── smtp.py
|
||
│ │ │ └── busylight.py
|
||
│ │ └── services/ # Business-Logik – kein HTTP-Wissen
|
||
│ │ ├── auth_service.py
|
||
│ │ ├── user_service.py
|
||
│ │ ├── absence_service.py
|
||
│ │ ├── time_service.py # inkl. ArbZG-Checks
|
||
│ │ ├── email_service.py
|
||
│ │ ├── ldap_service.py
|
||
│ │ └── caldav_service.py
|
||
│ ├── migrations/
|
||
│ │ ├── env.py # Alembic-Konfiguration
|
||
│ │ └── versions/ # 0001–0024 Migrationsdateien
|
||
│ ├── tests/
|
||
│ │ ├── conftest.py # pytest-Fixtures (session-scope, RLS-Setup)
|
||
│ │ ├── test_auth.py
|
||
│ │ ├── test_users.py
|
||
│ │ ├── test_absences.py
|
||
│ │ └── ...
|
||
│ ├── .env # Umgebungsvariablen (nie committen)
|
||
│ ├── alembic.ini
|
||
│ └── requirements.txt
|
||
├── frontend/
|
||
│ ├── src/
|
||
│ │ ├── api/ # Axios-Client-Funktionen pro Ressource
|
||
│ │ ├── components/
|
||
│ │ │ ├── Layout.tsx # Haupt-Layout mit Navigation
|
||
│ │ │ ├── Modal.tsx
|
||
│ │ │ ├── ProtectedRoute.tsx
|
||
│ │ │ ├── Spinner.tsx
|
||
│ │ │ └── absences/ # Abwesenheits-Unterkomponenten
|
||
│ │ ├── context/ # React Context: AuthContext
|
||
│ │ ├── hooks/ # Custom Hooks (useAuth, useAbsences, ...)
|
||
│ │ ├── pages/ # Eine Datei pro Seite/Route
|
||
│ │ │ ├── LoginPage.tsx
|
||
│ │ │ ├── DashboardPage.tsx
|
||
│ │ │ ├── TimeTrackingPage.tsx
|
||
│ │ │ ├── AbsencesPage.tsx
|
||
│ │ │ ├── UsersPage.tsx
|
||
│ │ │ ├── ReportsPage.tsx
|
||
│ │ │ ├── ProfilePage.tsx
|
||
│ │ │ ├── CompanySettingsPage.tsx
|
||
│ │ │ ├── KioskDevicesPage.tsx
|
||
│ │ │ ├── AuditLogPage.tsx
|
||
│ │ │ └── ...
|
||
│ │ ├── types/ # TypeScript-Interfaces
|
||
│ │ ├── utils/ # Hilfsfunktionen
|
||
│ │ ├── App.tsx # Router-Konfiguration
|
||
│ │ └── main.tsx
|
||
│ ├── dist/ # Build-Output (von nginx ausgeliefert)
|
||
│ └── package.json
|
||
├── nginx.conf
|
||
├── timemaster.service
|
||
├── setup_server.sh
|
||
└── update.sh
|
||
```
|
||
|
||
---
|
||
|
||
## Datenbankschema
|
||
|
||
### Tabellenübersicht und Beziehungen
|
||
|
||
```
|
||
companies
|
||
├──< departments (company_id)
|
||
├──< users (company_id)
|
||
│ ├──< sessions (user_id) -- Refresh-Token-Sessions
|
||
│ ├──< password_resets (user_id)
|
||
│ ├──< time_entries (user_id)
|
||
│ ├──< absences (user_id)
|
||
│ │ └──> absence_types (type_id)
|
||
│ ├──< vacation_balances (user_id)
|
||
│ └── work_schedule_id → work_schedules
|
||
├──< work_schedules (company_id)
|
||
├──< kiosk_devices (company_id)
|
||
├──< audit_logs (company_id)
|
||
├──< overtime_balances (company_id)
|
||
├── ldap_configs
|
||
├── smtp_configs
|
||
└── caldav_company_configs
|
||
```
|
||
|
||
### Wichtige Tabellen
|
||
|
||
**companies** – Mandanten-Stammdaten
|
||
- `id` UUID PK
|
||
- `name`, `slug` (URL-freundlich, unique)
|
||
- `sick_note_required_after_days` INTEGER DEFAULT 3
|
||
- `personnel_number_required` BOOLEAN DEFAULT FALSE
|
||
- `personnel_number_mode` VARCHAR(10) DEFAULT 'manual'
|
||
- `personnel_number_next` INTEGER DEFAULT 1
|
||
|
||
**users**
|
||
- `id`, `company_id`, `department_id`
|
||
- `email` unique, `password_hash`, `first_name`, `last_name`
|
||
- `role` ENUM: SUPER_ADMIN | COMPANY_ADMIN | HR | MANAGER | EMPLOYEE
|
||
- `auth_provider` ENUM: local | ldap
|
||
- `personnel_number` VARCHAR(50) – nur Ziffern, partial unique per Firma
|
||
- `totp_secret`, `totp_enabled` – TOTP 2FA
|
||
- `kiosk_pin_hash`, `kiosk_qr_token`
|
||
- `can_manual_time_entry` BOOLEAN DEFAULT FALSE
|
||
- `is_active` BOOLEAN – kein Hard-Delete
|
||
- `created_at`
|
||
|
||
**sessions** – Refresh-Token-Verwaltung
|
||
- `id`, `user_id`, `token_hash` (gehashter Refresh-Token)
|
||
- `expires_at`, `created_at`, `ip_address`, `user_agent`
|
||
- Bei Rotation: altes Token wird gelöscht, neues wird angelegt
|
||
|
||
**absence_types**
|
||
- `company_id`, `name`, `code`
|
||
- `affects_vacation` BOOLEAN – zieht vom Urlaubskonto ab
|
||
- `requires_approval` BOOLEAN
|
||
- `certificate_after_days` INTEGER NULL – per-Typ-Override für AU-Pflicht
|
||
- `color` – UI-Darstellung
|
||
|
||
**absences**
|
||
- `user_id`, `type_id`
|
||
- `start_date`, `end_date`, `half_day_start`, `half_day_end`
|
||
- `working_days` NUMERIC(5,1)
|
||
- `status` ENUM: pending | approved | rejected | cancelled
|
||
- `approved_by`, `substitute_id`
|
||
- `certificate_required_by` DATE – auto-berechnet bei SICK-Absences
|
||
- `certificate_received` BOOLEAN DEFAULT FALSE
|
||
- `meta` JSONB – flexible Zusatzdaten (Weiterbildung, Dienstreise etc.)
|
||
|
||
**vacation_balances**
|
||
- `user_id`, `year`
|
||
- `base_days`, `special_days`, `carried_over_days`, `used_days`
|
||
- `comment`
|
||
|
||
**time_entries**
|
||
- `user_id`, `date`
|
||
- `start_time`, `end_time`, `break_minutes`
|
||
- `working_minutes` COMPUTED
|
||
- `status` ENUM: draft | submitted | approved | rejected
|
||
- `source` ENUM: manual | stamp | import
|
||
- `arbzg_warning` – ArbZG-Verletzungs-Flag
|
||
|
||
**work_schedules**
|
||
- `company_id`, `name`
|
||
- `days_per_week`, wöchentliche Soll-Stunden
|
||
- Für ArbZG-Berechnung und Überstunden-Balance
|
||
|
||
**kiosk_devices**
|
||
- `company_id`, `name`, `location`
|
||
- `device_token_hash` – aktuell TOKEN-basiert (agent-08: Ed25519-Upgrade geplant)
|
||
- `is_active` – wird in 0021 durch `status` ENUM ersetzt
|
||
|
||
**audit_logs**
|
||
- `company_id`, `user_id`
|
||
- `action`, `entity_type`, `entity_id`
|
||
- `old_value`, `new_value` JSONB
|
||
- `ip_address`, `created_at`
|
||
|
||
**public_holidays** – global (keine RLS), pro Bundesland/Datum
|
||
|
||
### Alembic-Migrationskette
|
||
|
||
```
|
||
0001_initial companies, departments, users, sessions, password_resets, audit_logs
|
||
0002_time_entries time_entries, work_schedules
|
||
0003_absences absence_types, absences, vacation_balances, public_holidays
|
||
0004_ldap ldap_configs
|
||
0005_extensions Erweiterungsfelder (company/user)
|
||
0006_smtp smtp_configs
|
||
0007_caldav_and_fixes caldav_company_configs, caldav_user_configs; Bugfixes
|
||
0008_ldap_tls_verify ldap_configs.tls_verify Spalte
|
||
0009_absence_correction absences.correction_note
|
||
0010_public_holidays public_holidays.federal_state
|
||
0011_caldav_name_format caldav Name-Format-Felder
|
||
0012_caldav_template CalDAV-Template + users.kuerzel
|
||
0013_projects projects Tabelle (temporär)
|
||
0014_remove_projects projects Tabelle entfernt (Feature verschoben)
|
||
0015_totp users.totp_secret, totp_enabled
|
||
0016_vacation_special vacation_balances.special_days + Feiertagskalender-Erweiterungen
|
||
[0017 übersprungen] Nummer wurde nie in die Alembic-Kette aufgenommen
|
||
0018_kiosk_devices kiosk_devices Tabelle
|
||
0019_manual_time_entry users.can_manual_time_entry
|
||
0020_personnel_number users.personnel_number, companies.personnel_number_*
|
||
0021_kiosk_security TODO (agent-08): Ed25519 Public-Key, Enrollment, Heartbeat
|
||
0022_sick_note_config companies.sick_note_required_after_days
|
||
0023_busylight_pull companies.busylight_pull_token
|
||
0024_row_level_security PostgreSQL RLS auf allen Tabellen
|
||
```
|
||
|
||
Hinweis: Migration 0017 wurde übersprungen und existiert nicht in der Kette. Neuen Migrationen folgen auf 0024.
|
||
|
||
---
|
||
|
||
## Row Level Security (RLS)
|
||
|
||
Seit Migration 0024 setzt TimeMaster PostgreSQL Row Level Security für vollständige Mandanten-Isolation auf Datenbankebene ein. Selbst bei einem Fehler in der Applikationsschicht kann kein User Daten einer anderen Firma sehen.
|
||
|
||
### Prinzip
|
||
|
||
Jede Tabelle hat vier Policies (SELECT, INSERT, UPDATE, DELETE). Die Policies prüfen zwei PostgreSQL-Session-Variablen:
|
||
|
||
```sql
|
||
-- Bypass-Flag: 'on' = alle Zeilen sichtbar (für unauthentifizierte Endpunkte)
|
||
app.bypass_rls
|
||
|
||
-- Company-ID des eingeloggten Users (UUID als Text)
|
||
app.company_id
|
||
```
|
||
|
||
### Policy-Typen
|
||
|
||
**COMPANY_COL** – Tabellen mit direkter `company_id`-Spalte:
|
||
`absence_types, audit_logs, caldav_company_configs, departments, kiosk_devices, ldap_configs, overtime_balances, smtp_configs, users, work_schedules`
|
||
|
||
```sql
|
||
(COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on')
|
||
OR company_id = NULLIF(current_setting('app.company_id', true), '')::uuid
|
||
```
|
||
|
||
**USER_JOIN** – Tabellen ohne direkte `company_id`, verknüpft über `user_id`:
|
||
`absences, caldav_user_configs, password_resets, sessions, time_entries, vacation_balances`
|
||
|
||
```sql
|
||
(bypass) OR user_id IN (SELECT id FROM users WHERE company_id = ...)
|
||
```
|
||
|
||
**companies** selbst – Policy auf `id` (die Firma darf nur sich selbst sehen):
|
||
```sql
|
||
(bypass) OR id = NULLIF(current_setting('app.company_id', true), '')::uuid
|
||
```
|
||
|
||
### Ablauf pro HTTP-Request
|
||
|
||
1. `get_db()` öffnet eine AsyncSession, setzt `SET LOCAL app.bypass_rls = 'on'`
|
||
2. `get_current_user()` Dependency lädt den User (bypass noch aktiv)
|
||
3. Falls der User kein SUPER_ADMIN ist:
|
||
- `SET LOCAL app.company_id = '<uuid>'`
|
||
- `SET LOCAL app.bypass_rls = 'off'`
|
||
4. Alle folgenden Queries in dieser Transaktion sind automatisch auf die Firma des Users gefiltert
|
||
5. SUPER_ADMIN behält bypass aktiv – kann alle Firmen sehen
|
||
|
||
### expire_on_commit=False
|
||
|
||
Die `AsyncSessionLocal` ist mit `expire_on_commit=False` konfiguriert. Das ist zwingend notwendig: Nach einem `db.commit()` würde SQLAlchemy sonst alle Attribute als "expired" markieren und beim nächsten Zugriff einen neuen SELECT auslösen. Da der RLS-Kontext (`SET LOCAL`) transaktionsgebunden ist, wäre dieser zweite SELECT ggf. ohne Kontext – und würde entweder leer zurückkehren oder scheitern.
|
||
|
||
**Konsequenz:** `db.refresh(obj)` nach einem `commit()` ist verboten. Stattdessen beim `flush()` vor dem Commit die benötigten Werte sichern.
|
||
|
||
---
|
||
|
||
## Authentifizierungs-Flow
|
||
|
||
### Standard-Login (JWT + Refresh-Token)
|
||
|
||
```
|
||
Client FastAPI PostgreSQL / Redis
|
||
│ │ │
|
||
│── POST /api/v1/auth/login ─────>│ │
|
||
│ { email, password } │── SELECT user WHERE email ────>│
|
||
│ │<─ User-Objekt ─────────────────│
|
||
│ │── verify_password() │
|
||
│ │── INSERT sessions (token_hash)─>│
|
||
│<── { access_token, refresh_token } │
|
||
│ (JWT 30min, opaque 30 Tage) │
|
||
│ │ │
|
||
│── GET /api/v1/... (Bearer)─────>│ │
|
||
│ │── decode_access_token() │
|
||
│ │── db.get(User, user_id) │
|
||
│ │── SET LOCAL app.company_id │
|
||
│ │── SET LOCAL app.bypass_rls=off │
|
||
│<── Response ───────────────────│ │
|
||
│ │ │
|
||
│── POST /api/v1/auth/refresh────>│ │
|
||
│ { refresh_token } │── hash_token(refresh_token) │
|
||
│ │── SELECT sessions WHERE hash──>│
|
||
│ │── DELETE old session │
|
||
│ │── INSERT new session │
|
||
│<── { neue access_token, refresh_token } │
|
||
```
|
||
|
||
- Access Token: JWT (HS256), Payload: `sub` = user_id (UUID), `exp`
|
||
- Refresh Token: 64-byte-Zufallsstring, in DB nur als SHA-256-Hash gespeichert
|
||
- Rotation: Jedes Refresh erzeugt ein neues Token-Paar. Das alte Refresh-Token wird sofort invalidiert.
|
||
|
||
### TOTP / 2FA-Flow
|
||
|
||
Wenn `user.totp_enabled = True`:
|
||
|
||
```
|
||
POST /auth/login → { totp_required: true, partial_token: "<JWT>" }
|
||
↓
|
||
POST /auth/login/totp { partial_token, totp_code }
|
||
↓ pyotp.TOTP(secret).verify(code, valid_window=1)
|
||
↓
|
||
{ access_token, refresh_token } (vollständige Session)
|
||
```
|
||
|
||
TOTP-Setup (Profil-Seite):
|
||
1. `GET /auth/totp/setup` → `{ secret, qr_code_url }` (base32-Secret, otpauth:// URI)
|
||
2. User scannt QR in Authenticator-App
|
||
3. `POST /auth/totp/confirm { code }` – verifiziert ersten Code, setzt `totp_enabled = True`
|
||
4. `POST /auth/totp/disable { password, code }` – deaktiviert 2FA nach Bestätigung
|
||
|
||
### Rollen-Hierarchie
|
||
|
||
```
|
||
SUPER_ADMIN
|
||
└── COMPANY_ADMIN (Vollzugriff eigene Firma, sieht ALLE User der Firma)
|
||
├── HR (Personalakten, Reports, Attest-Verwaltung)
|
||
├── MANAGER (Genehmigungen für eigenes Team)
|
||
└── EMPLOYEE (Eigene Daten, eigene Anträge)
|
||
```
|
||
|
||
SUPER_ADMIN ist der Plattform-Betreiber und hat RLS-Bypass. COMPANY_ADMIN registriert sich selbst beim `/auth/register`-Endpunkt und legt damit automatisch eine neue Firma an.
|
||
|
||
---
|
||
|
||
## Datenfluss: Zeiterfassung
|
||
|
||
```
|
||
POST /time/stamp-in
|
||
→ time_service.stamp_in(user_id, db)
|
||
→ Prüfe: kein offener Eintrag für heute
|
||
→ INSERT time_entries(user_id, date, start_time, status='draft')
|
||
|
||
POST /time/stamp-out
|
||
→ time_service.stamp_out(user_id, db)
|
||
→ Hole offenen Eintrag
|
||
→ Berechne working_minutes = end - start - break_minutes
|
||
→ ArbZG-Check:
|
||
if working_minutes > 480: warn (max 8h; bis 600 möglich mit Warnung)
|
||
if working_minutes > 360 and break_minutes < 30: warn (Pause fehlt ab 6h)
|
||
if working_minutes > 540 and break_minutes < 45: warn (Pause fehlt ab 9h)
|
||
→ UPDATE time_entries(end_time, working_minutes, arbzg_warning)
|
||
|
||
PATCH /time/entries/{id}/approve [MANAGER/HR/ADMIN]
|
||
→ UPDATE status = 'approved'
|
||
→ INSERT audit_log
|
||
|
||
PATCH /time/entries/{id}/reject [MANAGER/HR/ADMIN]
|
||
→ UPDATE status = 'rejected', rejection_reason
|
||
```
|
||
|
||
---
|
||
|
||
## Datenfluss: Abwesenheiten
|
||
|
||
```
|
||
POST /absences (Antrag stellen)
|
||
→ absence_service.create(data, current_user, db)
|
||
→ Berechne working_days (Wochentage - Feiertage)
|
||
→ Prüfe Urlaubskonto falls affects_vacation = True
|
||
→ Auto-calculate certificate_required_by falls AbsenceType.code = 'SICK'
|
||
(start_date + sick_note_required_after_days - 1)
|
||
→ INSERT absences(status='pending')
|
||
→ INSERT audit_log
|
||
|
||
POST /absences/{id}/approve [MANAGER/HR/ADMIN]
|
||
→ UPDATE absences.status = 'approved', approved_by = current_user.id
|
||
→ Falls affects_vacation: UPDATE vacation_balances.used_days += working_days
|
||
→ INSERT audit_log
|
||
|
||
POST /absences/quick-sick [EMPLOYEE]
|
||
→ Holt ersten aktiven SICK-AbsenceType der Firma
|
||
→ Legt Absence mit status='approved' an (keine Genehmigung nötig)
|
||
→ Berechnet certificate_required_by
|
||
|
||
PATCH /absences/{id}/certificate [HR/ADMIN]
|
||
→ UPDATE absences.certificate_received = True
|
||
→ INSERT audit_log
|
||
|
||
GET /absences/sick-stats?user_id=&ref_date=
|
||
→ Berechnet Bradford-Faktor: S² × D
|
||
S = Episoden (Krankheitsepisoden im rollenden 12-Monats-Fenster)
|
||
D = Tage (Gesamtkranktage im selben Fenster)
|
||
```
|
||
|
||
---
|
||
|
||
## Mandanten-Isolation
|
||
|
||
Mandanten-Isolation ist in TimeMaster dreischichtig implementiert:
|
||
|
||
1. **Applikationsschicht**: Jeder Endpunkt, der Daten schreibt oder liest, wird über `CurrentUser` Dependency abgesichert. Beim Anlegen von Objekten wird immer `company_id = current_user.company_id` gesetzt. Beim Lesen filtert der Service aktiv nach `company_id`.
|
||
|
||
2. **Datenbankschicht (RLS)**: PostgreSQL Row Level Security (Migration 0024) verhindert Datenzugriff über Firmengrenzen auf DB-Ebene. Auch direkte psql-Verbindungen mit dem App-User werden gefiltert (FORCE ROW LEVEL SECURITY).
|
||
|
||
3. **Session-Kontext**: `SET LOCAL app.company_id` gilt nur für die aktuelle Transaktion. Jede neue Anfrage beginnt mit einem frischen Kontext.
|
||
|
||
SUPER_ADMIN ist die einzige Rolle mit `bypass_rls = 'on'` und kann alle Firmen sehen. Diese Rolle ist ausschließlich für den Plattform-Betreiber gedacht.
|