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