Add Detailed Role Documentation and Update References
- 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.
This commit is contained in:
@@ -193,6 +193,8 @@ See `docs/ARCHITECTURE.md` for full schema. Key tables:
|
||||
- Opt-in visibility: No role assignment = invisible to everyone
|
||||
- **Auto-assignment:** New users automatically receive `'private'` role on first login
|
||||
|
||||
**📖 Detaillierte Rollen-Dokumentation:** Siehe [`docs/ROLES.md`](./docs/ROLES.md) für vollständige Informationen zum Rollen-System (Auto-Assignment, Produktsichtbarkeit, Menüpunkt-Filterung, Session-Management, API-Endpoints, Code-Patterns).
|
||||
|
||||
Use Drizzle ORM for all database operations. Most tables use UUID primary keys (except `roles` which uses enum code as PK).
|
||||
|
||||
## Development Commands (Once Set Up)
|
||||
@@ -1209,6 +1211,7 @@ For complete implementation with all utilities (PKCE generator, Cidaas API clien
|
||||
- **PRD:** See `docs/PRD.md` for complete requirements
|
||||
- **Tech Stack:** See `docs/TECH_STACK.md` for technology decisions
|
||||
- **Architecture:** See `docs/ARCHITECTURE.md` for system design and data flows
|
||||
- **Roles:** See `docs/ROLES.md` for complete role system documentation (auto-assignment, visibility rules, API endpoints, code patterns)
|
||||
- **experimenta Info:** See `docs/EXPERIMENTA_INFO.md` for company information (address, opening hours, legal links, contact details)
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ Eine spezialisierte E-Commerce-App, die es Besuchern des experimenta Science Cen
|
||||
- Multi-Payment-Provider
|
||||
- Laborkurse
|
||||
|
||||
**📖 Detaillierte Dokumentation:** Siehe [`docs/ROLES.md`](./ROLES.md) für vollständige Informationen zum Rollen-System (Auto-Assignment, Produktsichtbarkeit, Menüpunkt-Filterung, API-Endpoints, Code-Beispiele).
|
||||
|
||||
---
|
||||
|
||||
## 3. Zielgruppen
|
||||
|
||||
524
docs/ROLES.md
Normal file
524
docs/ROLES.md
Normal file
@@ -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 |
|
||||
Reference in New Issue
Block a user