Browse Source
- Introduced comprehensive documentation for the role system in a new `docs/ROLES.md` file, covering role types, auto-assignment, product visibility, and menu visibility. - Updated `CLAUDE.md` and `docs/PRD.md` to reference the new roles documentation, enhancing clarity on role management and its implications for users. - Ensured all relevant documentation links are consistent and accessible for better user guidance.main
3 changed files with 529 additions and 0 deletions
@ -0,0 +1,524 @@ |
|||
# Rollen-System Dokumentation |
|||
|
|||
**Version:** 1.0 |
|||
**Letztes Update:** Januar 2025 |
|||
|
|||
--- |
|||
|
|||
## Überblick |
|||
|
|||
Das Rollen-System von my.experimenta.science ermöglicht es, Produkte und UI-Komponenten basierend auf der aktiven Rolle des Benutzers anzuzeigen oder zu verstecken. Dies ermöglicht rollenspezifische Preise, Produkte und Funktionen. |
|||
|
|||
--- |
|||
|
|||
## 1. Rollen-Typen |
|||
|
|||
### 1.1 Verfügbare Rollen |
|||
|
|||
| Code | Display Name | Beschreibung | Approval erforderlich | |
|||
|------|--------------|--------------|----------------------| |
|||
| `private` | Privatperson | Private Nutzung | Nein (auto-assigned) | |
|||
| `educator` | Pädagoge | Lehrkräfte und Schulen | Ja (Post-MVP) | |
|||
| `company` | Unternehmen | Geschäftskunden | Ja (Post-MVP) | |
|||
|
|||
### 1.2 TypeScript Type Definition |
|||
|
|||
```typescript |
|||
type RoleCode = 'private' | 'educator' | 'company' |
|||
|
|||
interface Role { |
|||
code: RoleCode // Primary key |
|||
displayName: string // "Privatperson", "Pädagoge", "Unternehmen" |
|||
description: string |
|||
requiresApproval: boolean // false for 'private', true for others |
|||
sortOrder: number |
|||
active: boolean |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 2. Automatische Rollen-Zuweisung |
|||
|
|||
### 2.1 Auto-Assignment bei First Login (MVP) |
|||
|
|||
Alle neuen Benutzer erhalten automatisch die Rolle `private` beim ersten Login: |
|||
|
|||
```typescript |
|||
// server/api/auth/login.post.ts |
|||
if (!user) { |
|||
// Neuer User - erstelle Profil |
|||
const [newUser] = await db.insert(users).values({...}).returning() |
|||
|
|||
// Auto-assign 'private' role |
|||
await assignRoleToUser(newUser.id, 'private', { |
|||
adminNotes: 'Auto-assigned on first login', |
|||
}) |
|||
} |
|||
``` |
|||
|
|||
**Wichtig:** |
|||
- Status ist immer `approved` (kein Approval-Workflow in MVP) |
|||
- Jeder User hat mindestens eine Rolle |
|||
- Safety-Check: Existing users ohne Rollen bekommen auch `private` role |
|||
|
|||
--- |
|||
|
|||
## 3. Rollen-basierte Produktsichtbarkeit |
|||
|
|||
### 3.1 Konzept |
|||
|
|||
Produkte sind NUR sichtbar, wenn: |
|||
1. Das Produkt `product_role_visibility` Einträge hat |
|||
2. Der User eine genehmigte (`approved`) Rolle hat, die in `product_role_visibility` vorkommt |
|||
|
|||
**Opt-in Visibility:** |
|||
- Produkt ohne `product_role_visibility` Einträge → Unsichtbar für ALLE |
|||
- User ohne genehmigte Rollen → Sieht KEINE Produkte |
|||
|
|||
### 3.2 Database Schema |
|||
|
|||
```sql |
|||
-- Many-to-Many: Product ↔ Roles |
|||
CREATE TABLE product_role_visibility ( |
|||
id UUID PRIMARY KEY, |
|||
product_id UUID NOT NULL REFERENCES products(id), |
|||
role_code TEXT NOT NULL REFERENCES roles(code), |
|||
UNIQUE(product_id, role_code) |
|||
); |
|||
``` |
|||
|
|||
### 3.3 Automatische Zuordnung (ERP Import) |
|||
|
|||
Beim Import aus NAV ERP werden Produkte automatisch Rollen zugeordnet: |
|||
|
|||
```typescript |
|||
// server/utils/roles.ts |
|||
const categoryRoleMapping = { |
|||
'makerspace-annual-pass': ['private', 'educator'], |
|||
'annual-pass': ['private'], |
|||
'educator-annual-pass': ['educator'], |
|||
'company-annual-pass': ['company'] |
|||
} |
|||
|
|||
// server/api/erp/products.post.ts |
|||
await assignRolesToProductByCategory(product.id, category) |
|||
``` |
|||
|
|||
### 3.4 API Filtering Pattern |
|||
|
|||
```typescript |
|||
// server/api/products/index.get.ts |
|||
export default defineEventHandler(async (event) => { |
|||
const { user } = await getUserSession(event) |
|||
|
|||
// Unauthenticated users see NO products |
|||
if (!user) return [] |
|||
|
|||
// Get visible product IDs for user's roles |
|||
const visibleProductIds = await getVisibleProductIdsForUser(user.id) |
|||
|
|||
// Fetch products with role filter |
|||
const products = await db.query.products.findMany({ |
|||
where: and( |
|||
eq(products.active, true), |
|||
inArray(products.id, visibleProductIds) |
|||
) |
|||
}) |
|||
|
|||
return products |
|||
}) |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 4. Rollen-basierte Menüpunkt-Sichtbarkeit |
|||
|
|||
### 4.1 Konzept |
|||
|
|||
Navigation-Tabs können für bestimmte Rollen ein-/ausgeblendet werden: |
|||
- `roleVisibility: 'all'` → Sichtbar für alle Rollen |
|||
- `roleVisibility: ['educator']` → Nur für Pädagogen |
|||
- `roleVisibility: ['educator', 'company']` → Für mehrere Rollen |
|||
|
|||
### 4.2 Implementation (AreaTabs.vue) |
|||
|
|||
```typescript |
|||
interface ProductArea { |
|||
id: string |
|||
label: string |
|||
icon: any |
|||
enabled: boolean |
|||
visible: boolean |
|||
badge?: string |
|||
route: string |
|||
roleVisibility?: 'all' | RoleCode[] // NEW |
|||
} |
|||
|
|||
const areas: ProductArea[] = [ |
|||
{ id: 'start', roleVisibility: 'all' }, // Alle Rollen |
|||
{ id: 'makerspace', roleVisibility: 'all' }, // Alle Rollen |
|||
{ id: 'educator', roleVisibility: ['educator'] }, // Nur Pädagogen |
|||
{ id: 'experimenta', roleVisibility: 'all' }, // Alle Rollen |
|||
] |
|||
|
|||
const { activeRole } = useActiveRole() |
|||
|
|||
const visibleAreas = computed(() => { |
|||
return areas.filter(area => { |
|||
if (!area.visible) return false |
|||
if (!area.roleVisibility) return true |
|||
if (area.roleVisibility === 'all') return true |
|||
return area.roleVisibility.includes(activeRole.value as RoleCode) |
|||
}) |
|||
}) |
|||
``` |
|||
|
|||
### 4.3 Automatische Updates |
|||
|
|||
Menüpunkte aktualisieren sich automatisch beim Rollenwechsel: |
|||
- ✅ Vue Computed Property reagiert auf `activeRole` Änderungen |
|||
- ✅ Keine manuellen Refresh-Calls nötig |
|||
- ✅ Sofortige UI-Updates |
|||
|
|||
--- |
|||
|
|||
## 5. Client-Side Composable |
|||
|
|||
### 5.1 useActiveRole() |
|||
|
|||
```typescript |
|||
// app/composables/useActiveRole.ts |
|||
export function useActiveRole() { |
|||
const activeRole = useState<string>('activeRole', () => 'private') |
|||
const roles = useState<RoleWithStatus[]>('roles', () => []) |
|||
|
|||
// Fetch current role status from server |
|||
async function fetchRoleStatus() { ... } |
|||
|
|||
// Switch to a different role |
|||
async function switchRole(roleCode: string) { |
|||
await $fetch('/api/user/active-role', { |
|||
method: 'PATCH', |
|||
body: { roleCode }, |
|||
}) |
|||
|
|||
activeRole.value = roleCode |
|||
|
|||
// Auto-refresh product pages |
|||
await Promise.all([ |
|||
refreshNuxtData('products-list'), |
|||
refreshNuxtData('educator-products'), |
|||
refreshNuxtData('experimenta-products'), |
|||
]) |
|||
} |
|||
|
|||
return { |
|||
activeRole, |
|||
roles, |
|||
approvedRoles: computed(() => roles.value.filter(r => r.hasRole)), |
|||
hasMultipleRoles: computed(() => approvedRoles.value.length > 1), |
|||
fetchRoleStatus, |
|||
switchRole, |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 5.2 Auto-Initialization |
|||
|
|||
Rollen werden automatisch beim Login geladen: |
|||
|
|||
```typescript |
|||
const { loggedIn } = useUserSession() |
|||
if (loggedIn.value) { |
|||
callOnce('init-roles', () => fetchRoleStatus()) |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 6. Server-Side Utilities |
|||
|
|||
### 6.1 Wichtige Functions |
|||
|
|||
```typescript |
|||
// server/utils/roles.ts |
|||
|
|||
// Get user's approved role codes |
|||
await getUserApprovedRoleCodes(userId) |
|||
// => ['private', 'educator'] |
|||
|
|||
// Get visible product IDs for user |
|||
await getVisibleProductIdsForUser(userId) |
|||
// => ['uuid-1', 'uuid-2', ...] |
|||
|
|||
// Get visible product IDs for specific role |
|||
await getVisibleProductIdsForRole(userId, roleCode) |
|||
// => ['uuid-1', 'uuid-2', ...] |
|||
|
|||
// Check if product is visible for user |
|||
await isProductVisibleForUser(productId, userId) |
|||
// => true/false |
|||
|
|||
// Assign role to user (MVP: always approved) |
|||
await assignRoleToUser(userId, 'private', { |
|||
adminNotes: 'Auto-assigned', |
|||
}) |
|||
|
|||
// Auto-assign roles to product by category |
|||
await assignRolesToProductByCategory(productId, 'annual-pass') |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 7. Session Management |
|||
|
|||
### 7.1 Active Role Session |
|||
|
|||
Die aktive Rolle wird server-side in der Session gespeichert: |
|||
|
|||
```typescript |
|||
// server/utils/role-session.ts |
|||
|
|||
// Get user's active role (with TTL validation) |
|||
const activeRole = await getUserActiveRole(event) |
|||
// => 'private' | 'educator' | 'company' |
|||
|
|||
// Set user's active role in session + DB |
|||
await setUserActiveRole(event, userId, 'educator') |
|||
|
|||
// Validate active role (TTL-based) |
|||
const isValid = await validateActiveRole(event, userId) |
|||
``` |
|||
|
|||
**Session TTL:** |
|||
- Active role ist 30 Tage gültig |
|||
- Nach Ablauf: Fallback auf erste approved role |
|||
- Update bei jedem `switchRole()` |
|||
|
|||
--- |
|||
|
|||
## 8. API Endpoints |
|||
|
|||
### 8.1 Role Management Endpoints |
|||
|
|||
| Endpoint | Method | Beschreibung | |
|||
|----------|--------|--------------| |
|||
| `/api/user/role-status` | GET | Aktuellen Role-Status abrufen | |
|||
| `/api/user/active-role` | PATCH | Aktive Rolle wechseln | |
|||
|
|||
#### GET /api/user/role-status |
|||
|
|||
**Response:** |
|||
```json |
|||
{ |
|||
"activeRoleCode": "private", |
|||
"roles": [ |
|||
{ |
|||
"code": "private", |
|||
"displayName": "Privatperson", |
|||
"description": "Private Nutzung", |
|||
"hasRole": true, |
|||
"requiresApproval": false |
|||
}, |
|||
{ |
|||
"code": "educator", |
|||
"displayName": "Pädagoge", |
|||
"description": "Lehrkräfte und Schulen", |
|||
"hasRole": true, |
|||
"requiresApproval": true |
|||
} |
|||
], |
|||
"roleChangedByAdmin": false |
|||
} |
|||
``` |
|||
|
|||
#### PATCH /api/user/active-role |
|||
|
|||
**Request:** |
|||
```json |
|||
{ |
|||
"roleCode": "educator" |
|||
} |
|||
``` |
|||
|
|||
**Validation:** |
|||
- ✅ User muss eingeloggt sein |
|||
- ✅ User muss die Rolle haben (approved status) |
|||
- ✅ Rolle muss existieren |
|||
|
|||
--- |
|||
|
|||
## 9. UI Components |
|||
|
|||
### 9.1 RoleSwitcher Component |
|||
|
|||
Der RoleSwitcher zeigt die aktive Rolle an und ermöglicht Rollenwechsel: |
|||
|
|||
```vue |
|||
<!-- app/components/navigation/RoleSwitcher.vue --> |
|||
<template> |
|||
<DropdownMenu> |
|||
<DropdownMenuTrigger> |
|||
Du kaufst als: {{ activeRoleDisplay }} |
|||
</DropdownMenuTrigger> |
|||
<DropdownMenuContent> |
|||
<DropdownMenuItem |
|||
v-for="role in approvedRoles" |
|||
@click="switchRole(role.code)" |
|||
> |
|||
{{ role.displayName }} |
|||
</DropdownMenuItem> |
|||
</DropdownMenuContent> |
|||
</DropdownMenu> |
|||
</template> |
|||
``` |
|||
|
|||
**Features:** |
|||
- ✅ Zeigt nur approved roles |
|||
- ✅ Markiert aktive Rolle mit Checkmark |
|||
- ✅ Disabled für roles die User nicht hat |
|||
- ✅ Triggert automatische Product-Refresh |
|||
|
|||
--- |
|||
|
|||
## 10. Testing |
|||
|
|||
### 10.1 Test User Setup |
|||
|
|||
```typescript |
|||
// Test User mit multiple roles |
|||
const user = await createTestUser({ |
|||
email: 'test@example.com', |
|||
roles: ['private', 'educator'] |
|||
}) |
|||
|
|||
// Setze aktive Rolle |
|||
await setUserActiveRole(event, user.id, 'educator') |
|||
``` |
|||
|
|||
### 10.2 Test Scenarios |
|||
|
|||
**Scenario 1: Privatperson Login** |
|||
- ✅ User erhält `private` role |
|||
- ✅ Sieht nur Produkte mit `private` role |
|||
- ✅ Sieht Navigation: Start, Makerspace, experimenta |
|||
|
|||
**Scenario 2: Pädagoge Login** |
|||
- ✅ User hat `private` + `educator` roles |
|||
- ✅ Kann zwischen Rollen wechseln |
|||
- ✅ Als Pädagoge: Zusätzliche Produkte sichtbar |
|||
- ✅ Als Pädagoge: "Bildung" Tab sichtbar |
|||
|
|||
**Scenario 3: Rollenwechsel** |
|||
- ✅ Produktliste aktualisiert sich automatisch |
|||
- ✅ Navigation aktualisiert sich automatisch |
|||
- ✅ Session + DB werden aktualisiert |
|||
- ✅ Product detail page: 404 redirect wenn nicht sichtbar |
|||
|
|||
--- |
|||
|
|||
## 11. MVP Limitations |
|||
|
|||
### 11.1 Was NICHT implementiert ist (Post-MVP) |
|||
|
|||
**Approval Workflow:** |
|||
- ❌ Keine UI für Rollen-Antrag (Pädagogen/Unternehmen) |
|||
- ❌ Kein Admin-Panel für Genehmigung |
|||
- ❌ Status bleibt immer `approved` |
|||
|
|||
**Role Management:** |
|||
- ❌ Keine Self-Service Rollen-Verwaltung |
|||
- ❌ Keine Admin-Funktionen zum Hinzufügen/Entfernen von Rollen |
|||
- ❌ Keine Rollen-History für Admins |
|||
|
|||
**Advanced Features:** |
|||
- ❌ Keine Organisation-Level Rollen (z.B. alle Lehrer einer Schule) |
|||
- ❌ Keine zeitlich begrenzte Rollen |
|||
- ❌ Keine Rollen-Hierarchien |
|||
|
|||
### 11.2 Database prepared for Post-MVP |
|||
|
|||
Trotz MVP Limitations ist die Datenbank bereits vorbereitet: |
|||
|
|||
```typescript |
|||
// user_roles table (prepared but status always 'approved' in MVP) |
|||
interface UserRole { |
|||
status: 'pending' | 'approved' | 'rejected' | 'revoked' |
|||
organizationName?: string // Prepared |
|||
requestMessage?: string // Prepared |
|||
proofDocument?: string // Prepared |
|||
adminNotes?: string // Prepared |
|||
statusHistory: StatusHistoryEntry[] // JSONB audit trail |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 12. Best Practices |
|||
|
|||
### 12.1 Beim Hinzufügen neuer Rollen |
|||
|
|||
1. **Database:** Füge neue Rolle zu `roles` table hinzu |
|||
2. **Type:** Update `RoleCode` type in schema |
|||
3. **Mapping:** Update `categoryRoleMapping` in `server/utils/roles.ts` |
|||
4. **UI:** Update RoleSwitcher display names |
|||
5. **Navigation:** Update `roleVisibility` in AreaTabs.vue |
|||
|
|||
### 12.2 Beim Hinzufügen neuer Produkte |
|||
|
|||
1. **Category:** Definiere eine klare Kategorie |
|||
2. **Mapping:** Füge Kategorie zu `categoryRoleMapping` hinzu |
|||
3. **Visibility:** Produkt wird automatisch Rollen zugeordnet beim Import |
|||
|
|||
### 12.3 Beim Hinzufügen neuer Navigation-Items |
|||
|
|||
```typescript |
|||
{ |
|||
id: 'new-tab', |
|||
label: 'Neuer Tab', |
|||
roleVisibility: 'all' // oder ['educator', 'company'] |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 13. Troubleshooting |
|||
|
|||
### Problem: User sieht keine Produkte |
|||
|
|||
**Prüfen:** |
|||
1. Hat User eine approved Rolle? → `getUserApprovedRoleCodes(userId)` |
|||
2. Hat Produkt `product_role_visibility` Einträge? |
|||
3. Matchen Rolle und Produkt-Visibility? |
|||
|
|||
### Problem: Menüpunkt erscheint nicht nach Rollenwechsel |
|||
|
|||
**Prüfen:** |
|||
1. Ist `roleVisibility` korrekt gesetzt? |
|||
2. Ist `activeRole` korrekt aktualisiert? → Vue DevTools |
|||
3. Ist computed property `visibleAreas` reaktiv? |
|||
|
|||
### Problem: Produkt bleibt sichtbar nach Rollenwechsel |
|||
|
|||
**Prüfen:** |
|||
1. Wurde `refreshNuxtData()` aufgerufen in `switchRole()`? |
|||
2. Sind die richtigen keys verwendet? (`products-list`, etc.) |
|||
3. Hat API-Endpoint role-based filtering? |
|||
|
|||
--- |
|||
|
|||
## 14. Weitere Dokumentation |
|||
|
|||
- **Architektur:** `docs/ARCHITECTURE.md` - Database Schema Details |
|||
- **PRD:** `docs/PRD.md` - Product Requirements |
|||
- **Testing:** `docs/TESTING.md` - Test Credentials & Scenarios |
|||
- **Cidaas Integration:** `docs/CIDAAS_INTEGRATION.md` - Auth Flow |
|||
|
|||
--- |
|||
|
|||
## 15. Changelog |
|||
|
|||
| Version | Datum | Änderung | |
|||
|---------|-------|----------| |
|||
| 1.0 | Jan 2025 | Initial documentation - MVP implementation | |
|||
Loading…
Reference in new issue