feat(260928-ujj): Dashboard-Hintergrund pro Benutzer in der Datenbank
- Spalte User.dashboardBackground (JSONB) samt Migration - PATCH /users/me/dashboard-background, geprueft mit parseDashboardBackground aus @tessera/shared (Allowlist, UUID-Bildkennung) - getMe liefert dashboardBackground normalisiert neben accentColor - Web liest die Wahl aus dem Auth-Store, speichert ueber die Server-Aktion, alte localStorage-Wahl wird einmalig uebernommen - Hinweistext: gilt auf jedem Geraet Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- quick-260928-ujj: Dashboard-Hintergrund pro Benutzer in der Datenbank.
|
||||
--
|
||||
-- Bisher lag die Wahl des Dashboard-Hintergrunds (Design "Mosaik") im
|
||||
-- localStorage des Browsers und folgte dem Benutzer nicht auf ein anderes
|
||||
-- Geraet oder in die Desktop-App. Jetzt steht sie hier, geschrieben nur ueber
|
||||
-- PATCH /users/me/dashboard-background und dort wie beim Lesen durch
|
||||
-- parseDashboardBackground (@tessera/shared) geprueft und normalisiert.
|
||||
-- NULL = nie gewaehlt (das Web uebernimmt dann einmalig eine alte
|
||||
-- localStorage-Wahl); sonst ein Objekt { kind: 'none' | 'preset' | 'image', ... }.
|
||||
-- Bewusst kein Standardwert und kein Backfill.
|
||||
--
|
||||
-- Die Anmelde-Funktionen auth_lookup_* liefern eine feste Spaltenliste
|
||||
-- (RETURNS TABLE) und bleiben von der neuen Spalte unberuehrt.
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "dashboardBackground" JSONB;
|
||||
@@ -46,6 +46,10 @@ model User {
|
||||
// quick-260925-bow: zuletzt gesehene freigegebene Version (X.Y.Z) fuer das
|
||||
// "Was ist neu"-Fenster; null = Bestandsbenutzer (sieht nur die laufende Version)
|
||||
lastSeenReleaseVersion String?
|
||||
// quick-260928-ujj: gewaehlter Dashboard-Hintergrund; null = nie gewaehlt,
|
||||
// sonst das durch parseDashboardBackground (@tessera/shared) normalisierte
|
||||
// Objekt, auch { kind: 'none' } fuer bewusst "kein Hintergrund"
|
||||
dashboardBackground Json?
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
groupMemberships GroupMembership[]
|
||||
moduleGrants ModuleGrant[]
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Role } from '@prisma/client';
|
||||
import { parseDashboardBackground } from '@tessera/shared';
|
||||
import * as argon2 from 'argon2';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Response } from 'express';
|
||||
@@ -331,6 +332,7 @@ export class AuthService {
|
||||
ldapDn: true,
|
||||
avatarPath: true,
|
||||
accentColor: true,
|
||||
dashboardBackground: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -338,10 +340,13 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { passwordHash, ldapDn, avatarPath, ...publicFields } = user;
|
||||
const { passwordHash, ldapDn, avatarPath, dashboardBackground, ...publicFields } = user;
|
||||
|
||||
return {
|
||||
...publicFields,
|
||||
// quick-260928-ujj (T-ujj-01): auch beim Lesen durch die gemeinsame
|
||||
// Pruefregel — NULL oder ein ungueltiger Inhalt ergibt null.
|
||||
dashboardBackground: parseDashboardBackground(dashboardBackground),
|
||||
isLocalUser: !!passwordHash && !ldapDn,
|
||||
hasAvatar: !!avatarPath,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Role } from '@prisma/client';
|
||||
import {
|
||||
compareReleaseVersions,
|
||||
type DashboardBackground,
|
||||
parseDashboardBackground,
|
||||
parseReleaseVersion,
|
||||
type ReleaseNoticeResponse,
|
||||
} from '@tessera/shared';
|
||||
@@ -62,6 +64,10 @@ function resolveAvatarsDir(): string {
|
||||
* Selbstbedienungswege `GET me/release-notice` und `POST me/release-seen`
|
||||
* ("Was ist neu"-Fenster), ebenfalls `forTenant()` mit
|
||||
* `where: { id: currentUser.id }`.
|
||||
*
|
||||
* quick-260928-ujj: dazu kommt ein gebundener Zugriff des
|
||||
* Selbstbedienungswegs `PATCH me/dashboard-background`, ebenfalls
|
||||
* `forTenant()` mit `where: { id: currentUser.id }`.
|
||||
*/
|
||||
@Controller('users')
|
||||
@UseGuards(RolesGuard)
|
||||
@@ -483,6 +489,43 @@ export class UserController {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /users/me/dashboard-background (quick-260928-ujj)
|
||||
*
|
||||
* Speichert den gewaehlten Dashboard-Hintergrund des angemeldeten
|
||||
* Benutzers. Jeder angemeldete Benutzer, kein `@Roles`.
|
||||
*
|
||||
* T-ujj-01 (Tampering): der Wert wird spaeter als CSS-Hintergrund
|
||||
* gerendert. `parseDashboardBackground` (@tessera/shared) laesst nur
|
||||
* `kind` none/preset/image, bekannte Preset-Kennungen und eine UUID als
|
||||
* Bildkennung zu und baut ein frisches Objekt ohne Zusatzschluessel;
|
||||
* alles andere ergibt 400 ohne Schreibzugriff. Bewusst Inline-Body-Typ
|
||||
* statt DTO-Klasse (wie `me/accent-color`): die globale ValidationPipe mit
|
||||
* `whitelist` wuerde das verschachtelte Objekt sonst nicht pruefen.
|
||||
*
|
||||
* T-ujj-02 (Elevation of Privilege): kein Kennungsparameter; geschrieben
|
||||
* wird ausschliesslich die eigene Zeile (`where: { id: currentUser.id }`)
|
||||
* ueber `forTenant(this.prisma, currentUser.tenantId)`.
|
||||
*/
|
||||
@Patch('me/dashboard-background')
|
||||
async updateDashboardBackground(
|
||||
@Body() body: { background: unknown },
|
||||
@CurrentUser() currentUser: AuthUser,
|
||||
): Promise<{ success: true; dashboardBackground: DashboardBackground }> {
|
||||
const background = parseDashboardBackground(body?.background);
|
||||
if (background === null) {
|
||||
throw new BadRequestException('Invalid dashboard background.');
|
||||
}
|
||||
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId);
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: currentUser.id },
|
||||
data: { dashboardBackground: background },
|
||||
});
|
||||
|
||||
return { success: true, dashboardBackground: background };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /users/me/avatar
|
||||
* Stream the current user's avatar image.
|
||||
|
||||
Reference in New Issue
Block a user