feat(260925-bow): gesehene Version pro Benutzer merken - Spalte, Versionsfunktionen, API
- packages/shared: parseReleaseVersion, compareReleaseVersions, ReleaseNoticeResponse - getRunningRelease(): einzige Quelle der laufenden Version (APP_VERSION der API) - User.lastSeenReleaseVersion (nullbar, Migration 20260925120000) - GET /users/me/release-notice, POST /users/me/release-seen (gebunden an Benutzer und Mandant) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
@@ -16,6 +18,11 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Role } from '@prisma/client';
|
||||
import {
|
||||
compareReleaseVersions,
|
||||
parseReleaseVersion,
|
||||
type ReleaseNoticeResponse,
|
||||
} from '@tessera/shared';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Response } from 'express';
|
||||
@@ -25,7 +32,9 @@ import type { AuthUser, UploadedFileLike } from '../auth/types/auth-user';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { getRunningRelease } from '../health/app-version';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { ReleaseSeenDto } from './dto/release-seen.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@@ -48,6 +57,11 @@ function resolveAvatarsDir(): string {
|
||||
* gebundenen Klienten (`tenantPrisma`, Konvention aus `ldap`, `groups`,
|
||||
* `dkv`, `auth`, `user.service.ts`) oder ueber die uebergreifenden Methoden
|
||||
* von `UserService`, deren Rumpf je Mandant gebunden ist.
|
||||
*
|
||||
* quick-260925-bow: dazu kommen drei gebundene Zugriffe der beiden
|
||||
* Selbstbedienungswege `GET me/release-notice` und `POST me/release-seen`
|
||||
* ("Was ist neu"-Fenster), ebenfalls `forTenant()` mit
|
||||
* `where: { id: currentUser.id }`.
|
||||
*/
|
||||
@Controller('users')
|
||||
@UseGuards(RolesGuard)
|
||||
@@ -111,6 +125,93 @@ export class UserController {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /users/me/release-notice (quick-260925-bow, D-03/D-04)
|
||||
*
|
||||
* Grundlage des "Was ist neu"-Fensters: die laufende freigegebene Version
|
||||
* der API (`getRunningRelease()`, `null` auf `dev`-Staenden) und der
|
||||
* gemerkte Stand des angemeldeten Benutzers (`null` = Bestandsbenutzer).
|
||||
* Welche Abschnitte der Aenderungsliste gezeigt werden, entscheidet das Web.
|
||||
*
|
||||
* Jeder angemeldete Benutzer, kein `@Roles`. Mandantenbindung: gelesen wird
|
||||
* ausschliesslich die eigene Zeile (`where: { id: currentUser.id }`) ueber
|
||||
* `forTenant(this.prisma, currentUser.tenantId)`; es gibt keinen
|
||||
* Kennungsparameter. Statische `me/...`-Route steht VOR der Kennungs-Route (GET :id).
|
||||
*/
|
||||
@Get('me/release-notice')
|
||||
async getReleaseNotice(@CurrentUser() currentUser: AuthUser): Promise<ReleaseNoticeResponse> {
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId);
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: currentUser.id },
|
||||
select: { lastSeenReleaseVersion: true },
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return {
|
||||
currentRelease: getRunningRelease(),
|
||||
lastSeenReleaseVersion: user.lastSeenReleaseVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /users/me/release-seen (quick-260925-bow, D-05)
|
||||
*
|
||||
* Merkt beim Schliessen des Fensters die gesehene Version. Angenommen wird
|
||||
* nur die kanonische Form `X.Y.Z`, die nicht ueber der laufenden Version
|
||||
* liegt; ohne freigegebene laufende Version (`dev`) wird nichts gemerkt.
|
||||
* Ein gemerkter Stand wird nie abgesenkt; ein unparsebarer gemerkter Wert
|
||||
* wird ueberschrieben. Die DTO prueft das Format schon in der
|
||||
* ValidationPipe; die Pruefungen im Rumpf gelten zusaetzlich (Unit-Tests
|
||||
* rufen die Methode ohne Pipe auf).
|
||||
*
|
||||
* Jeder angemeldete Benutzer, kein `@Roles`. Mandantenbindung wie oben:
|
||||
* Lesen und Schreiben nur der eigenen Zeile ueber den an
|
||||
* `currentUser.tenantId` gebundenen Klienten.
|
||||
*/
|
||||
@Post('me/release-seen')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async markReleaseSeen(
|
||||
@Body() body: ReleaseSeenDto,
|
||||
@CurrentUser() currentUser: AuthUser,
|
||||
): Promise<{ success: true; lastSeenReleaseVersion: string }> {
|
||||
const version: unknown = body?.version;
|
||||
if (typeof version !== 'string' || parseReleaseVersion(version) !== version) {
|
||||
throw new BadRequestException('Invalid version. Use X.Y.Z.');
|
||||
}
|
||||
const running = getRunningRelease();
|
||||
if (running === null) {
|
||||
throw new BadRequestException('No released version is running.');
|
||||
}
|
||||
if (compareReleaseVersions(version, running) > 0) {
|
||||
throw new BadRequestException('Version is newer than the running version.');
|
||||
}
|
||||
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId);
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: currentUser.id },
|
||||
select: { lastSeenReleaseVersion: true },
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const stored = user.lastSeenReleaseVersion;
|
||||
if (
|
||||
stored !== null &&
|
||||
parseReleaseVersion(stored) !== null &&
|
||||
compareReleaseVersions(version, stored) <= 0
|
||||
) {
|
||||
return { success: true, lastSeenReleaseVersion: stored };
|
||||
}
|
||||
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: currentUser.id },
|
||||
data: { lastSeenReleaseVersion: version },
|
||||
});
|
||||
return { success: true, lastSeenReleaseVersion: version };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /users/:id
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user