feat(admin): Anmeldehinweise der Willkommensmail in der Vorlage bearbeitbar
Neue Felder loginHintDirectory/loginHintLocal (Platzhalter, Pflicht, max. 1000), Migration 20260930170000 (nullable, leer = Standard aus @tessera/shared). Knopf Passwort festlegen und Gueltigkeitshinweis bleiben fest; local-no-link wird nie erzeugt und bleibt fest. Vorschau springt beim Bearbeiten auf die passende Kontoart. Lokal im Browser nachgewiesen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
* Anmeldehinweis" `WelcomeMailService`.
|
||||
*
|
||||
* Eigene Vorlage (Administrator → Willkommensmail): Betreff, Ueberschrift,
|
||||
* Einleitung und Abschluss kommen als `texts` herein (Vorlage des Mandanten
|
||||
* Einleitung, die zwei Anmeldehinweise (Verzeichniskonto / lokales Konto)
|
||||
* und Abschluss kommen als `texts` herein (Vorlage des Mandanten
|
||||
* des ZIEL-Benutzers, sonst `DEFAULT_WELCOME_MAIL_TEXTS` aus
|
||||
* `@tessera/shared`). Platzhalter (`{{name}}`, `{{vorname}}`,
|
||||
* `{{benutzername}}`, `{{email}}`, `{{adresse}}`, `{{firma}}`) werden auf dem
|
||||
@@ -62,6 +63,11 @@ const FONT =
|
||||
export type WelcomeMailAccount =
|
||||
| { kind: 'directory' }
|
||||
| { kind: 'local'; setPasswordUrl: string; validHours: number }
|
||||
/**
|
||||
* Lokales Konto ohne Link. Wird heute nirgends erzeugt (`WelcomeMailService`
|
||||
* legt fuer lokale Konten immer einen Token an); deshalb bleibt ihr Hinweis
|
||||
* fest und ist nicht Teil der Vorlage.
|
||||
*/
|
||||
| { kind: 'local-no-link' }
|
||||
/**
|
||||
* Nur Testmail aus Administrator → Willkommensmail: derselbe Baustein wie
|
||||
@@ -159,13 +165,22 @@ function safeUrl(value: string): string {
|
||||
return /^https?:\/\//i.test(value) ? value : '';
|
||||
}
|
||||
|
||||
function loginHintText(account: WelcomeMailAccount): string {
|
||||
/**
|
||||
* Anmeldehinweis je Kontoart als REINER Text (Platzhalter noch nicht
|
||||
* ersetzt). Verzeichnis- und lokale Konten nehmen den Text der Vorlage; ist
|
||||
* er leer oder fehlt er (aeltere Aufrufer), gilt der Standardtext.
|
||||
*/
|
||||
function loginHintText(account: WelcomeMailAccount, texts: Partial<WelcomeMailTexts>): string {
|
||||
const pick = (field: 'loginHintDirectory' | 'loginHintLocal') => {
|
||||
const value = texts[field];
|
||||
return typeof value === 'string' && value.trim() ? value : DEFAULT_WELCOME_MAIL_TEXTS[field];
|
||||
};
|
||||
switch (account.kind) {
|
||||
case 'directory':
|
||||
return 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.';
|
||||
return pick('loginHintDirectory');
|
||||
case 'local':
|
||||
case 'local-example':
|
||||
return 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.';
|
||||
return pick('loginHintLocal');
|
||||
case 'local-no-link':
|
||||
return 'Ihr Startpasswort erhalten Sie von Ihrem Administrator.';
|
||||
}
|
||||
@@ -248,18 +263,19 @@ function headerRow(src: string | null): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Willkommensmail. Die vier Texte (Betreff, Ueberschrift,
|
||||
* Einleitung, Abschluss) kommen aus der eigenen Vorlage des Mandanten, sonst
|
||||
* Baut die Willkommensmail. Die sechs Texte (Betreff, Ueberschrift,
|
||||
* Einleitung, Anmeldehinweis je Kontoart, Abschluss) kommen aus der eigenen
|
||||
* Vorlage des Mandanten, sonst
|
||||
* aus `DEFAULT_WELCOME_MAIL_TEXTS`. Sie sind REINER Text: Platzhalter werden
|
||||
* auf dem Text ersetzt, erst danach wird alles escaped — HTML aus der
|
||||
* Vorlage oder aus einem Benutzerwert erscheint als Text, nie als Markup.
|
||||
* Kopf, Kasten Adresse/Benutzername, Anmeldehinweis, Knopf "Zu Tessera" und
|
||||
* Fusszeile sind feste Bausteine und nicht Teil der Vorlage.
|
||||
* Kopf, Kasten Adresse/Benutzername, Knopf "Passwort festlegen" mit
|
||||
* Gueltigkeitshinweis, Knopf "Zu Tessera" und Fusszeile sind feste Bausteine
|
||||
* und nicht Teil der Vorlage.
|
||||
*/
|
||||
export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail {
|
||||
const base = safeUrl(input.appUrl.replace(/\/+$/, ''));
|
||||
const loginUrl = `${base}/login`;
|
||||
const hint = loginHintText(input.account);
|
||||
const texts = input.texts ?? DEFAULT_WELCOME_MAIL_TEXTS;
|
||||
|
||||
const values: WelcomeMailPlaceholderValues = {
|
||||
@@ -275,6 +291,7 @@ export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail
|
||||
const subject = singleLine(fill(texts.subject)) || WELCOME_MAIL_SUBJECT;
|
||||
const heading = singleLine(fill(texts.heading));
|
||||
const introParagraphs = paragraphsOf(fill(texts.intro));
|
||||
const hintParagraphs = paragraphsOf(fill(loginHintText(input.account, texts)));
|
||||
const closingParagraphs = paragraphsOf(fill(texts.closing));
|
||||
const notice = input.notice ? singleLine(input.notice) : '';
|
||||
|
||||
@@ -283,13 +300,8 @@ export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail
|
||||
if (notice) textLines.push(`[${notice}]`, '');
|
||||
if (heading) textLines.push(heading, '');
|
||||
for (const para of introParagraphs) textLines.push(para, '');
|
||||
textLines.push(
|
||||
'Ihre Zugangsdaten',
|
||||
`Adresse: ${base}`,
|
||||
`Benutzername: ${input.username}`,
|
||||
'',
|
||||
hint,
|
||||
);
|
||||
textLines.push('Ihre Zugangsdaten', `Adresse: ${base}`, `Benutzername: ${input.username}`);
|
||||
for (const para of hintParagraphs) textLines.push('', para);
|
||||
if (input.account.kind === 'local') {
|
||||
textLines.push(
|
||||
'',
|
||||
@@ -318,7 +330,9 @@ export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail
|
||||
const label = (content: string) =>
|
||||
`<div style="font-family:${FONT};font-size:12px;line-height:16px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:${C.muted};">${content}</div>`;
|
||||
|
||||
let accountBlock = p(escapeHtml(hint), 'margin:24px 0 16px;');
|
||||
let accountBlock = hintParagraphs
|
||||
.map((part, i) => p(para(part), i === 0 ? 'margin:24px 0 16px;' : ''))
|
||||
.join('\n');
|
||||
if (input.account.kind === 'local') {
|
||||
const setUrl = safeUrl(input.account.setPasswordUrl);
|
||||
accountBlock += `${button(setUrl, 'Passwort festlegen', C.ink, '#ffffff')}
|
||||
|
||||
Reference in New Issue
Block a user