feat(users): Willkommensmail mit Wellen-Kopf und Logo, Spalte Letzte Anmeldung
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 1m18s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m11s

POST /users/:id/welcome-mail (gleiche Rechte wie Bearbeiten, jederzeit sendbar),
GET /users/welcome-mail/status; HTML-Mail (Tabellenlayout, Inline-Stile,
Kopfbild als CID-PNG aus assets/mail/welcome-header.svg, erzeugt mit
scripts/render-mail-header.mjs) plus Textfassung. Verzeichniskonten: Hinweis
auf Windows-Passwort; lokale Konten: Link Passwort festlegen (7 Tage, einmalig).
Neue Spalte User.welcomeMailSentAt (Migration 20260930120000). Benutzerliste:
Spalte Letzte Anmeldung, Zeilenaktionen als Symbole. Dockerfile kopiert
apps/api/assets. Lokal per MailHog nachgewiesen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 15:28:52 +02:00
parent 0b34e82b21
commit 32441d77c7
24 changed files with 1540 additions and 115 deletions
+66
View File
@@ -6,6 +6,7 @@ import {
ForbiddenException,
Get,
HttpCode,
Headers,
HttpStatus,
NotFoundException,
Param,
@@ -39,6 +40,7 @@ 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';
import { WelcomeMailService } from './welcome-mail.service';
/** Map accepted MIME types to file extensions (T-gbh-01). */
const AVATAR_MIME_TO_EXT: Record<string, string> = {
@@ -68,6 +70,10 @@ function resolveAvatarsDir(): string {
* quick-260928-ujj: dazu kommt ein gebundener Zugriff des
* Selbstbedienungswegs `PATCH me/dashboard-background`, ebenfalls
* `forTenant()` mit `where: { id: currentUser.id }`.
*
* Willkommensmail: `POST :id/welcome-mail` loest den Zielbenutzer ueber
* `resolveTargetUser` auf; die beiden Schreibzugriffe (Token, Versandzeit)
* liegen gebunden in `WelcomeMailService`, nicht in diesem Controller.
*/
@Controller('users')
@UseGuards(RolesGuard)
@@ -75,6 +81,7 @@ export class UserController {
constructor(
private readonly userService: UserService,
private readonly prisma: PrismaService,
private readonly welcomeMailService: WelcomeMailService,
) {}
/**
@@ -126,6 +133,7 @@ export class UserController {
tenantId: true,
createdAt: true,
lastLoginAt: true,
welcomeMailSentAt: true,
},
orderBy: { username: 'asc' },
});
@@ -218,6 +226,23 @@ export class UserController {
return { success: true, lastSeenReleaseVersion: version };
}
/**
* GET /users/welcome-mail/status (Willkommensmail)
*
* Ob fuer den Mandanten des Aufrufers ein Versandweg eingerichtet ist —
* die Oberflaeche deaktiviert damit den Knopf "Willkommensmail senden"
* und nennt den Grund. Die wirksame Pruefung bleibt beim Versand selbst
* (`POST :id/welcome-mail`, dort je Mandant des ZIELS). Statische Route
* steht VOR der Kennungs-Route (GET :id).
*/
@Get('welcome-mail/status')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async getWelcomeMailStatus(
@CurrentUser() currentUser: AuthUser,
): Promise<{ available: boolean }> {
return { available: await this.welcomeMailService.isAvailable(currentUser.tenantId) };
}
/**
* GET /users/:id
*/
@@ -330,6 +355,47 @@ export class UserController {
return result;
}
/**
* POST /users/:id/welcome-mail (Willkommensmail)
*
* Schickt einem Benutzer (egal ob schon angemeldet) eine
* Willkommensmail (Adresse, Benutzername, Anmeldehinweis je Kontoart) und
* merkt den Zeitpunkt in `welcomeMailSentAt`. Dieselben Regeln wie beim
* Bearbeiten: ADMIN nur im eigenen Mandanten, SUPER_ADMIN uebergreifend;
* ein Nicht-SUPER_ADMIN darf einem SUPER_ADMIN nichts schicken
* (Zielrollen-Riegel wie in update()). Deaktiviert -> 409, ohne
* Adresse -> 400, ohne Versandweg -> 409, Versandfehler -> 502 (alles in
* `WelcomeMailService`). Der Origin-Kopf dient nur als Rueckfall fuer die
* Adresse in der Mail, wenn `TESSERA_APP_URL` fehlt.
*/
@Post(':id/welcome-mail')
@HttpCode(HttpStatus.OK)
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async sendWelcomeMail(
@Param('id') id: string,
@CurrentUser() currentUser: AuthUser,
@Headers('origin') origin?: string,
): Promise<{ success: true; to: string; welcomeMailSentAt: Date }> {
const user = await this.resolveTargetUser(currentUser, id);
if (!user) {
throw new NotFoundException('User not found');
}
if (
currentUser.role !== Role.SUPER_ADMIN &&
user.tenantId !== currentUser.tenantId
) {
throw new ForbiddenException('Cannot modify users from other tenants');
}
if (user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN) {
throw new ForbiddenException('Cannot modify a SUPER_ADMIN user');
}
const result = await this.welcomeMailService.send(user, origin);
return { success: true, ...result };
}
/**
* DELETE /users/:id
* ADMIN cannot delete self or users from other tenants.