feat(users): Willkommensmail mit Wellen-Kopf und Logo, Spalte Letzte Anmeldung
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user