mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 17:46:42 +02:00
✨ feat(auth): add centralized user settings synced across all apps
- Add settings module to mana-core-auth with REST API endpoints - Create user_settings table with globalSettings and appOverrides (JSONB) - Add createUserSettingsStore() factory in shared-theme package - Integrate user settings in all app layouts (calendar, chat, contacts, etc.) - Support for nav position, theme, locale settings with per-app overrides - Optimistic updates with localStorage caching for offline support - Add comprehensive documentation in docs/USER_SETTINGS.md API Endpoints: - GET /api/v1/settings - Get all user settings - PATCH /api/v1/settings/global - Update global settings - PATCH /api/v1/settings/app/:appId - Set app override - DELETE /api/v1/settings/app/:appId - Remove app override 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0f2aae631d
commit
0e5d923faf
30 changed files with 1624 additions and 7 deletions
357
docs/USER_SETTINGS.md
Normal file
357
docs/USER_SETTINGS.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# Zentrale User Settings
|
||||
|
||||
Die User Settings werden zentral in `mana-core-auth` gespeichert und über alle Apps synchronisiert. Dies ermöglicht eine konsistente Benutzererfahrung über das gesamte Mana-Ökosystem.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ mana-core-auth │
|
||||
│ (Port 3001) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ auth.user_settings Table │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ user_id │ globalSettings │ appOverrides │ │ │
|
||||
│ │ │─────────│────────────────│──────────────────────│ │ │
|
||||
│ │ │ uuid │ {nav, theme, │ {"calendar": {...}, │ │ │
|
||||
│ │ │ │ locale} │ "chat": {...}} │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ REST API (JWT Auth)
|
||||
│
|
||||
┌──────────────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│Calendar │ │ Chat │ │ Picture │
|
||||
│ App │ │ App │ │ App │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## Datenstruktur
|
||||
|
||||
### Global Settings
|
||||
|
||||
Globale Einstellungen gelten für **alle Apps** als Standardwerte:
|
||||
|
||||
```typescript
|
||||
interface GlobalSettings {
|
||||
nav: {
|
||||
desktopPosition: 'top' | 'bottom'; // Position der Navigation auf Desktop
|
||||
sidebarCollapsed: boolean; // Sidebar eingeklappt?
|
||||
};
|
||||
theme: {
|
||||
mode: 'light' | 'dark' | 'system'; // Theme-Modus
|
||||
colorScheme: string; // 'ocean' | 'nature' | 'lume' | 'stone'
|
||||
};
|
||||
locale: string; // 'de' | 'en' | 'fr' | 'es' | 'it'
|
||||
}
|
||||
```
|
||||
|
||||
**Standardwerte:**
|
||||
```typescript
|
||||
{
|
||||
nav: { desktopPosition: 'top', sidebarCollapsed: false },
|
||||
theme: { mode: 'system', colorScheme: 'ocean' },
|
||||
locale: 'de'
|
||||
}
|
||||
```
|
||||
|
||||
### App Overrides
|
||||
|
||||
Jede App kann die globalen Einstellungen überschreiben:
|
||||
|
||||
```typescript
|
||||
interface AppOverride {
|
||||
nav?: Partial<NavSettings>;
|
||||
theme?: Partial<ThemeSettings>;
|
||||
}
|
||||
|
||||
// Beispiel: Calendar hat eigene Einstellungen
|
||||
{
|
||||
"calendar": {
|
||||
nav: { desktopPosition: 'bottom' },
|
||||
theme: { colorScheme: 'nature' }
|
||||
},
|
||||
"chat": {
|
||||
theme: { mode: 'dark' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cascading / Auflösung
|
||||
|
||||
Die effektiven Settings einer App werden durch Merging berechnet:
|
||||
|
||||
```
|
||||
Effektive Settings = Global Settings + App Override (falls vorhanden)
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
- Global: `{ theme: { mode: 'system', colorScheme: 'ocean' } }`
|
||||
- Calendar Override: `{ theme: { colorScheme: 'nature' } }`
|
||||
- **Effektiv für Calendar:** `{ theme: { mode: 'system', colorScheme: 'nature' } }`
|
||||
|
||||
## Backend API
|
||||
|
||||
### Endpoints
|
||||
|
||||
Alle Endpoints erfordern JWT-Authentifizierung via `Authorization: Bearer <token>`.
|
||||
|
||||
| Endpoint | Method | Beschreibung |
|
||||
|----------|--------|--------------|
|
||||
| `/api/v1/settings` | GET | Alle Settings des Users abrufen |
|
||||
| `/api/v1/settings/global` | PATCH | Globale Settings aktualisieren |
|
||||
| `/api/v1/settings/app/:appId` | PATCH | App-Override setzen/aktualisieren |
|
||||
| `/api/v1/settings/app/:appId` | DELETE | App-Override löschen (zurück zu Global) |
|
||||
|
||||
### Beispiel-Requests
|
||||
|
||||
```bash
|
||||
# Token holen
|
||||
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "user@example.com", "password": "password"}' | jq -r '.accessToken')
|
||||
|
||||
# Alle Settings abrufen
|
||||
curl http://localhost:3001/api/v1/settings \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# Response:
|
||||
{
|
||||
"success": true,
|
||||
"globalSettings": {
|
||||
"nav": { "desktopPosition": "top", "sidebarCollapsed": false },
|
||||
"theme": { "mode": "system", "colorScheme": "ocean" },
|
||||
"locale": "de"
|
||||
},
|
||||
"appOverrides": {}
|
||||
}
|
||||
|
||||
# Sprache global ändern
|
||||
curl -X PATCH http://localhost:3001/api/v1/settings/global \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"locale": "en"}'
|
||||
|
||||
# Theme nur für Calendar überschreiben
|
||||
curl -X PATCH http://localhost:3001/api/v1/settings/app/calendar \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"theme": {"colorScheme": "nature"}}'
|
||||
|
||||
# Calendar-Override löschen (zurück zu Global)
|
||||
curl -X DELETE http://localhost:3001/api/v1/settings/app/calendar \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Store erstellen
|
||||
|
||||
Jede App erstellt einen eigenen User Settings Store in `src/lib/stores/user-settings.svelte.ts`:
|
||||
|
||||
```typescript
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'calendar', // Eindeutige App-ID
|
||||
authUrl: 'http://localhost:3001',
|
||||
getAccessToken: () => authStore.getAccessToken()
|
||||
});
|
||||
```
|
||||
|
||||
### Store laden
|
||||
|
||||
Im Root-Layout nach erfolgreicher Authentifizierung:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Settings lesen
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
</script>
|
||||
|
||||
<!-- Resolved Settings (Global + App Override) -->
|
||||
<p>Nav Position: {userSettings.nav.desktopPosition}</p>
|
||||
<p>Theme Mode: {userSettings.theme.mode}</p>
|
||||
<p>Locale: {userSettings.locale}</p>
|
||||
|
||||
<!-- Status -->
|
||||
<p>Loading: {userSettings.syncing}</p>
|
||||
<p>Has Override: {userSettings.hasAppOverride}</p>
|
||||
```
|
||||
|
||||
### Settings ändern
|
||||
|
||||
```typescript
|
||||
// Globale Settings ändern (gilt für alle Apps)
|
||||
await userSettings.updateGlobal({
|
||||
locale: 'en',
|
||||
theme: { mode: 'dark' }
|
||||
});
|
||||
|
||||
// App-Override setzen (nur diese App)
|
||||
await userSettings.updateAppOverride({
|
||||
nav: { desktopPosition: 'bottom' },
|
||||
theme: { colorScheme: 'nature' }
|
||||
});
|
||||
|
||||
// App-Override löschen (zurück zu Global)
|
||||
await userSettings.removeAppOverride();
|
||||
```
|
||||
|
||||
### Beispiel: PillNavigation
|
||||
|
||||
```svelte
|
||||
<PillNavigation
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
## Store API Referenz
|
||||
|
||||
### Properties (readonly)
|
||||
|
||||
| Property | Type | Beschreibung |
|
||||
|----------|------|--------------|
|
||||
| `nav` | `NavSettings` | Resolved Navigation Settings |
|
||||
| `theme` | `ThemeSettings` | Resolved Theme Settings |
|
||||
| `locale` | `string` | Aktuelle Sprache |
|
||||
| `globalSettings` | `GlobalSettings` | Rohe globale Settings |
|
||||
| `hasAppOverride` | `boolean` | Hat diese App einen Override? |
|
||||
| `syncing` | `boolean` | Wird gerade synchronisiert? |
|
||||
| `loaded` | `boolean` | Wurden Settings geladen? |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Beschreibung |
|
||||
|--------|--------------|
|
||||
| `load()` | Settings vom Server laden |
|
||||
| `updateGlobal(settings)` | Globale Settings aktualisieren |
|
||||
| `updateAppOverride(settings)` | App-Override setzen |
|
||||
| `removeAppOverride()` | App-Override löschen |
|
||||
|
||||
## Features
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
Änderungen werden sofort in der UI angezeigt, bevor die Server-Antwort eintrifft. Bei einem Fehler wird automatisch auf den vorherigen Zustand zurückgesetzt.
|
||||
|
||||
### localStorage Cache
|
||||
|
||||
Settings werden lokal gecached für:
|
||||
- Schnelle UI beim App-Start (keine Wartezeit auf Server)
|
||||
- Offline-Unterstützung (letzte bekannte Settings)
|
||||
|
||||
Cache-Key: `manacore-user-settings-{appId}`
|
||||
|
||||
### Deep Merge
|
||||
|
||||
Partielle Updates werden automatisch gemerged:
|
||||
|
||||
```typescript
|
||||
// Nur colorScheme ändern, mode bleibt erhalten
|
||||
await userSettings.updateGlobal({
|
||||
theme: { colorScheme: 'nature' }
|
||||
});
|
||||
// Ergebnis: { theme: { mode: 'system', colorScheme: 'nature' } }
|
||||
```
|
||||
|
||||
## Datenbank-Schema
|
||||
|
||||
Tabelle: `auth.user_settings`
|
||||
|
||||
| Column | Type | Beschreibung |
|
||||
|--------|------|--------------|
|
||||
| `user_id` | TEXT (PK) | Referenz zu auth.users |
|
||||
| `global_settings` | JSONB | Globale Einstellungen |
|
||||
| `app_overrides` | JSONB | Pro-App Überschreibungen |
|
||||
| `created_at` | TIMESTAMP | Erstellungszeitpunkt |
|
||||
| `updated_at` | TIMESTAMP | Letzte Aktualisierung |
|
||||
|
||||
## Dateien
|
||||
|
||||
### Backend (mana-core-auth)
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `src/settings/settings.module.ts` | NestJS Module |
|
||||
| `src/settings/settings.controller.ts` | REST API Endpoints |
|
||||
| `src/settings/settings.service.ts` | Business Logic |
|
||||
| `src/settings/dto/index.ts` | DTOs für Requests |
|
||||
| `src/db/schema/auth.schema.ts` | Drizzle Schema (`userSettings`) |
|
||||
|
||||
### Shared Package
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `packages/shared-theme/src/user-settings-store.svelte.ts` | Svelte 5 Store Factory |
|
||||
| `packages/shared-theme/src/types.ts` | TypeScript Interfaces |
|
||||
|
||||
### App Integration (Beispiel Calendar)
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts` | Store-Instanz |
|
||||
| `apps/calendar/apps/web/src/routes/+layout.svelte` | Integration in Layout |
|
||||
|
||||
## Integrierte Apps
|
||||
|
||||
Folgende Apps nutzen bereits die zentralen User Settings:
|
||||
|
||||
- Calendar (`calendar`)
|
||||
- Chat (`chat`)
|
||||
- Contacts (`contacts`)
|
||||
- ManaCore (`manacore`)
|
||||
- ManaDeck (`manadeck`)
|
||||
- Picture (`picture`)
|
||||
- Presi (`presi`)
|
||||
- Storage (`storage`)
|
||||
- Zitare (`zitare`)
|
||||
|
||||
## Neue App integrieren
|
||||
|
||||
1. **Store erstellen** in `src/lib/stores/user-settings.svelte.ts`:
|
||||
```typescript
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'my-app', // Eindeutige ID
|
||||
authUrl: import.meta.env.PUBLIC_MANA_CORE_AUTH_URL,
|
||||
getAccessToken: () => authStore.getAccessToken()
|
||||
});
|
||||
```
|
||||
|
||||
2. **Im Layout laden** nach Auth-Check:
|
||||
```typescript
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Settings verwenden**:
|
||||
```svelte
|
||||
<PillNavigation desktopPosition={userSettings.nav.desktopPosition} />
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue