From 714f731ac9ef93594f478ffd1d87875241f7c336 Mon Sep 17 00:00:00 2001
From: Schalli
Date: Wed, 30 Sep 2026 17:56:05 +0200
Subject: [PATCH] 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)
---
CHANGELOG.md | 2 +-
.../migration.sql | 18 +++++
apps/api/prisma/schema.prisma | 8 +-
.../src/mail/welcome-mail.template.spec.ts | 78 ++++++++++++++++++-
apps/api/src/mail/welcome-mail.template.ts | 48 ++++++++----
.../src/user/dto/welcome-mail-template.dto.ts | 18 ++++-
.../welcome-mail-template.controller.spec.ts | 32 +++++++-
.../user/welcome-mail-template.controller.ts | 11 ++-
.../welcome-mail-template.service.spec.ts | 30 ++++++-
.../src/user/welcome-mail-template.service.ts | 23 +++++-
.../api/src/user/welcome-mail.service.spec.ts | 32 +++++++-
.../welcome-mail-template-editor.test.tsx | 73 ++++++++++++++++-
.../admin/welcome-mail-template-editor.tsx | 42 ++++++++--
apps/web/src/lib/welcome-mail-template-api.ts | 4 +-
apps/web/src/messages/de.json | 6 +-
apps/web/src/messages/en.json | 6 +-
docs/anleitung-administration.md | 10 ++-
packages/shared/src/index.ts | 23 ++++--
18 files changed, 411 insertions(+), 53 deletions(-)
create mode 100644 apps/api/prisma/migrations/20260930170000_welcome_mail_login_hints/migration.sql
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17c3478..0298eb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
### Neu
-- Verwaltung: Eigene Vorlage für die Willkommensmail unter Administrator → Willkommensmail. Betreff, Überschrift, Einleitung und Abschluss lassen sich anpassen, mit Platzhaltern wie {{vorname}}, {{benutzername}}, {{adresse}} oder {{firma}} (eine Tabelle auf der Seite erklärt sie). Die Seite zeigt eine Live-Vorschau der echten Mail und schickt auf Wunsch eine Testmail an Ihre Adresse; „Auf Standard zurücksetzen“ stellt die mitgelieferten Texte wieder her. Logo, Zugangsdaten, Anmeldehinweis und Knöpfe fügt Tessera immer selbst ein.
+- Verwaltung: Eigene Vorlage für die Willkommensmail unter Administrator → Willkommensmail. Betreff, Überschrift, Einleitung, Abschluss und der Anmeldehinweis (getrennt für Verzeichnis- und lokale Konten) lassen sich anpassen, mit Platzhaltern wie {{vorname}}, {{benutzername}}, {{adresse}} oder {{firma}} (eine Tabelle auf der Seite erklärt sie). Die Seite zeigt eine Live-Vorschau der echten Mail und schickt auf Wunsch eine Testmail an Ihre Adresse; „Auf Standard zurücksetzen“ stellt die mitgelieferten Texte wieder her. Logo, Zugangsdaten und Knöpfe fügt Tessera immer selbst ein.
- Benutzerverwaltung: Willkommensmail. Über das Briefsymbol in der Benutzerliste schicken Sie einem Benutzer eine gestaltete Willkommensmail mit Tessera-Logo, Adresse, Benutzername und einem Knopf „Zu Tessera“. Konten aus dem Verzeichnis erhalten den Hinweis auf ihr Windows-Passwort, lokale Konten einen Link „Passwort festlegen“ (7 Tage gültig) – ein Passwort steht nie in der Mail. Die Liste zeigt, wann die Mail zuletzt ging.
- Benutzerverwaltung: Neue Spalte „Letzte Anmeldung“.
diff --git a/apps/api/prisma/migrations/20260930170000_welcome_mail_login_hints/migration.sql b/apps/api/prisma/migrations/20260930170000_welcome_mail_login_hints/migration.sql
new file mode 100644
index 0000000..c00ef81
--- /dev/null
+++ b/apps/api/prisma/migrations/20260930170000_welcome_mail_login_hints/migration.sql
@@ -0,0 +1,18 @@
+-- Willkommensmail: Anmeldehinweis je Kontoart als Teil der eigenen Vorlage.
+--
+-- Zweck: zwei neue Spalten in "WelcomeMailTemplate" —
+-- "loginHintDirectory" (Hinweis fuer verzeichnisgefuehrte Konten, Standard
+-- "Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten
+-- Windows-Passwort an.") und "loginHintLocal" (Hinweis vor dem Knopf
+-- "Passwort festlegen" fuer lokale Konten).
+--
+-- Bewusst NULLABLE ohne Default: bestehende Vorlagen behalten NULL, und
+-- WelcomeMailTemplateService setzt dafuer den Standardtext aus
+-- @tessera/shared ein. So steht der Standardtext nur an EINER Stelle im
+-- Code und nicht zusaetzlich in der Datenbank.
+--
+-- Zeilenschutz: unveraendert (tenant_isolation_policy der Tabelle gilt fuer
+-- die neuen Spalten mit).
+
+ALTER TABLE "WelcomeMailTemplate" ADD COLUMN "loginHintDirectory" TEXT;
+ALTER TABLE "WelcomeMailTemplate" ADD COLUMN "loginHintLocal" TEXT;
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 50eefbc..781a962 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -770,14 +770,18 @@ model Reminder {
// Eigene Vorlage der Willkommensmail (Administrator → Willkommensmail):
// hoechstens eine je Mandant; fehlt sie, gelten die Standardtexte aus
-// @tessera/shared (DEFAULT_WELCOME_MAIL_TEXTS). Nur die vier Texte —
-// Kopf, Zugangsdaten, Anmeldehinweis und Knoepfe bleiben fest im Code.
+// @tessera/shared (DEFAULT_WELCOME_MAIL_TEXTS). Nur die sechs Texte —
+// Kopf, Zugangsdaten und Knoepfe bleiben fest im Code.
model WelcomeMailTemplate {
id String @id @default(uuid())
tenantId String @unique
subject String
heading String
intro String
+ // Anmeldehinweise je Kontoart (Migration 20260930170000); NULL in einer
+ // aelteren Vorlage = Standardtext aus @tessera/shared
+ loginHintDirectory String?
+ loginHintLocal String?
closing String
// Benutzername des Administrators, der zuletzt gespeichert hat (Anzeige)
updatedBy String?
diff --git a/apps/api/src/mail/welcome-mail.template.spec.ts b/apps/api/src/mail/welcome-mail.template.spec.ts
index 5e56095..254294e 100644
--- a/apps/api/src/mail/welcome-mail.template.spec.ts
+++ b/apps/api/src/mail/welcome-mail.template.spec.ts
@@ -22,6 +22,18 @@ const base: WelcomeMailInput = {
headerImageSrc: null,
};
+/** Standard-Anmeldehinweise, damit Vorlagen im Test vollstaendig sind. */
+const hints = {
+ loginHintDirectory: DEFAULT_WELCOME_MAIL_TEXTS.loginHintDirectory,
+ loginHintLocal: DEFAULT_WELCOME_MAIL_TEXTS.loginHintLocal,
+};
+
+const localAccount = {
+ kind: 'local',
+ setPasswordUrl: 'https://tessera.example.invalid/reset-password/abc',
+ validHours: 168,
+} as const;
+
const values = {
name: 'Max Muster',
vorname: 'Max',
@@ -75,6 +87,7 @@ describe('renderWelcomeMail mit Vorlage', () => {
subject: 'Hallo\r\n{{firma}}',
heading: '{{name}}
',
intro: 'klick',
+ ...hints,
closing: '',
},
});
@@ -89,7 +102,7 @@ describe('renderWelcomeMail mit Vorlage', () => {
it('Leerzeile = neuer Absatz, einfacher Umbruch =
; leerer Abschluss faellt weg', () => {
const mail = renderWelcomeMail({
...base,
- texts: { subject: 's', heading: 'h', intro: 'A1\nA2\r\n\r\n\nB', closing: ' ' },
+ texts: { subject: 's', heading: 'h', intro: 'A1\nA2\r\n\r\n\nB', ...hints, closing: ' ' },
});
expect(mail.html).toContain('>A1
A2
');
expect(mail.html).toContain('>B');
@@ -105,7 +118,7 @@ describe('renderWelcomeMail mit Vorlage', () => {
setPasswordUrl: 'https://tessera.example.invalid/reset-password/abc',
validHours: 168,
},
- texts: { subject: 'x', heading: 'y', intro: '', closing: '' },
+ texts: { subject: 'x', heading: 'y', intro: '', ...hints, closing: '' },
});
expect(mail.html).toContain('https://tessera.example.invalid/reset-password/abc');
expect(mail.html).toContain('Passwort festlegen');
@@ -128,6 +141,67 @@ describe('renderWelcomeMail mit Vorlage', () => {
});
});
+describe('Anmeldehinweis je Kontoart', () => {
+ it('Standard: Verzeichniskonto bekommt den Windows-Hinweis, lokales Konto den Hinweis vor dem Knopf', () => {
+ const dir = renderWelcomeMail(base);
+ expect(dir.html).toContain('gewohnten Windows-Passwort');
+ expect(dir.html).not.toContain('persönliches Passwort fest');
+ const local = renderWelcomeMail({ ...base, account: localAccount });
+ expect(local.html).toContain('legen Sie bitte Ihr persönliches Passwort fest');
+ expect(local.html).not.toContain('Windows-Passwort');
+ // Hinweis steht vor dem Knopf
+ expect(local.html.indexOf('persönliches Passwort fest')).toBeLessThan(
+ local.html.indexOf('Passwort festlegen'),
+ );
+ });
+
+ it('eigener Text je Kontoart: Platzhalter ersetzt, escaped, Leerzeile = Absatz; Knopf und Gueltigkeit bleiben fest', () => {
+ const texts = {
+ ...DEFAULT_WELCOME_MAIL_TEXTS,
+ loginHintDirectory:
+ 'Anmeldung als {{benutzername}} mit Firmenkennwort.\n\nFragen? IT anrufen.',
+ loginHintLocal: 'Hallo {{vorname}}, bitte zuerst ein Passwort setzen.',
+ };
+ const dir = renderWelcomeMail({ ...base, texts });
+ expect(dir.html).toContain(
+ 'Anmeldung als max.muster mit <b>Firmenkennwort</b>.',
+ );
+ expect(dir.html).toContain('>Fragen? IT anrufen.');
+ expect(dir.html).not.toContain('Windows-Passwort');
+ expect(dir.html).not.toContain('bitte zuerst ein Passwort setzen');
+ expect(dir.text).toContain(
+ 'Anmeldung als max.muster mit Firmenkennwort.\n\nFragen? IT anrufen.',
+ );
+
+ const local = renderWelcomeMail({ ...base, account: localAccount, texts });
+ expect(local.html).toContain('Hallo Max, bitte zuerst ein Passwort setzen.');
+ expect(local.html).not.toContain('Firmenkennwort');
+ expect(local.html).toContain('reset-password/abc');
+ expect(local.html).toContain('7 Tage');
+ expect(local.text).toContain('Hallo Max, bitte zuerst ein Passwort setzen.');
+
+ const example = renderWelcomeMail({ ...base, account: { kind: 'local-example' }, texts });
+ expect(example.html).toContain('Hallo Max, bitte zuerst ein Passwort setzen.');
+ });
+
+ it('leerer oder fehlender Hinweis (aeltere Aufrufer) → Standardtext', () => {
+ const { loginHintDirectory: _d, loginHintLocal: _l, ...old } = DEFAULT_WELCOME_MAIL_TEXTS;
+ const dir = renderWelcomeMail({ ...base, texts: old as any });
+ expect(dir.html).toContain('gewohnten Windows-Passwort');
+ const local = renderWelcomeMail({
+ ...base,
+ account: localAccount,
+ texts: { ...DEFAULT_WELCOME_MAIL_TEXTS, loginHintLocal: ' ' },
+ });
+ expect(local.html).toContain('persönliches Passwort fest');
+ });
+
+ it('local-no-link behaelt den festen Hinweis', () => {
+ const mail = renderWelcomeMail({ ...base, account: { kind: 'local-no-link' } });
+ expect(mail.html).toContain('Ihr Startpasswort erhalten Sie von Ihrem Administrator.');
+ });
+});
+
describe('Kopf (Outlook ohne CID-Bild)', () => {
it('Wellenstreifen 600x40 in einer Zelle mit dunkler Kopffarbe, Inhalt beginnt mit 24px Abstand', () => {
const mail = renderWelcomeMail({ ...base, headerImageSrc: 'cid:welcome-header@tessera' });
diff --git a/apps/api/src/mail/welcome-mail.template.ts b/apps/api/src/mail/welcome-mail.template.ts
index 86c4fbf..c93590b 100644
--- a/apps/api/src/mail/welcome-mail.template.ts
+++ b/apps/api/src/mail/welcome-mail.template.ts
@@ -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): 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) =>
`${content}
`;
- 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')}
diff --git a/apps/api/src/user/dto/welcome-mail-template.dto.ts b/apps/api/src/user/dto/welcome-mail-template.dto.ts
index 5c4021b..f73ba9f 100644
--- a/apps/api/src/user/dto/welcome-mail-template.dto.ts
+++ b/apps/api/src/user/dto/welcome-mail-template.dto.ts
@@ -20,6 +20,8 @@ const FIELD_LABELS: Record = {
subject: 'Betreff',
heading: 'Überschrift',
intro: 'Einleitungstext',
+ loginHintDirectory: 'Anmeldehinweis für Verzeichniskonten',
+ loginHintLocal: 'Anmeldehinweis für lokale Konten',
closing: 'Abschlusstext',
};
@@ -48,8 +50,8 @@ class NurBekanntePlatzhalterConstraint implements ValidatorConstraintInterface {
* Inhalt der eigenen Vorlage (PUT /welcome-mail-template, auch Grundlage fuer
* Vorschau und Testmail). Reiner Text: HTML wird beim Rendern escaped, nicht
* hier abgewiesen — ein "<" im Text ist erlaubt und erscheint als Zeichen.
- * Betreff und Ueberschrift sind Pflicht, Einleitung und Abschluss duerfen
- * leer sein (der Abschnitt faellt dann weg).
+ * Betreff, Ueberschrift und beide Anmeldehinweise sind Pflicht, Einleitung
+ * und Abschluss duerfen leer sein (der Abschnitt faellt dann weg).
*/
export class WelcomeMailTemplateDto implements WelcomeMailTexts {
@IsString()
@@ -69,6 +71,18 @@ export class WelcomeMailTemplateDto implements WelcomeMailTexts {
@Validate(NurBekanntePlatzhalterConstraint)
intro!: string;
+ @IsString()
+ @Matches(/\S/, { message: 'Bitte geben Sie einen Anmeldehinweis für Verzeichniskonten ein.' })
+ @MaxLength(WELCOME_MAIL_LIMITS.loginHintDirectory)
+ @Validate(NurBekanntePlatzhalterConstraint)
+ loginHintDirectory!: string;
+
+ @IsString()
+ @Matches(/\S/, { message: 'Bitte geben Sie einen Anmeldehinweis für lokale Konten ein.' })
+ @MaxLength(WELCOME_MAIL_LIMITS.loginHintLocal)
+ @Validate(NurBekanntePlatzhalterConstraint)
+ loginHintLocal!: string;
+
@IsString()
@MaxLength(WELCOME_MAIL_LIMITS.closing)
@Validate(NurBekanntePlatzhalterConstraint)
diff --git a/apps/api/src/user/welcome-mail-template.controller.spec.ts b/apps/api/src/user/welcome-mail-template.controller.spec.ts
index df2519f..2dba100 100644
--- a/apps/api/src/user/welcome-mail-template.controller.spec.ts
+++ b/apps/api/src/user/welcome-mail-template.controller.spec.ts
@@ -115,7 +115,7 @@ describe('WelcomeMailTemplateDto (globale ValidationPipe)', () => {
expect(messages).toContain('Betreff: Unbekannter Platzhalter {{xyz}}');
});
- it('Laengen: Betreff/Ueberschrift 200, Texte 4000', async () => {
+ it('Laengen: Betreff/Ueberschrift 200, Anmeldehinweise 1000, Texte 4000', async () => {
await expect(
validateBody({ ...valid, subject: 'a'.repeat(200), intro: 'b'.repeat(4000) }),
).resolves.toBeTruthy();
@@ -123,6 +123,19 @@ describe('WelcomeMailTemplateDto (globale ValidationPipe)', () => {
await expect(messagesOf({ ...valid, heading: 'a'.repeat(201) })).resolves.toHaveLength(1);
await expect(messagesOf({ ...valid, intro: 'a'.repeat(4001) })).resolves.toHaveLength(1);
await expect(messagesOf({ ...valid, closing: 'a'.repeat(4001) })).resolves.toHaveLength(1);
+ await expect(
+ validateBody({
+ ...valid,
+ loginHintDirectory: 'a'.repeat(1000),
+ loginHintLocal: 'b'.repeat(1000),
+ }),
+ ).resolves.toBeTruthy();
+ await expect(
+ messagesOf({ ...valid, loginHintDirectory: 'a'.repeat(1001) }),
+ ).resolves.toHaveLength(1);
+ await expect(messagesOf({ ...valid, loginHintLocal: 'a'.repeat(1001) })).resolves.toHaveLength(
+ 1,
+ );
});
it('leerer Betreff/leere Ueberschrift → 400; leere Einleitung/Abschluss erlaubt', async () => {
@@ -135,6 +148,23 @@ describe('WelcomeMailTemplateDto (globale ValidationPipe)', () => {
await expect(validateBody({ ...valid, intro: '', closing: '' })).resolves.toBeTruthy();
});
+ it('Anmeldehinweise: Pflicht, Platzhalter geprueft, Leerzeilen erlaubt', async () => {
+ expect(await messagesOf({ ...valid, loginHintDirectory: ' \n ' })).toContain(
+ 'Bitte geben Sie einen Anmeldehinweis für Verzeichniskonten ein.',
+ );
+ expect(await messagesOf({ ...valid, loginHintLocal: '' })).toContain(
+ 'Bitte geben Sie einen Anmeldehinweis für lokale Konten ein.',
+ );
+ expect(await messagesOf({ ...valid, loginHintLocal: 'Hallo {{kennwort}}' })).toContain(
+ 'Anmeldehinweis für lokale Konten: Unbekannter Platzhalter {{kennwort}}',
+ );
+ await expect(
+ validateBody({ ...valid, loginHintDirectory: 'Hallo {{ Vorname }}\n\nZweiter Absatz' }),
+ ).resolves.toBeTruthy();
+ const { loginHintLocal: _l, ...rest } = valid;
+ await expect(messagesOf(rest)).resolves.not.toHaveLength(0);
+ });
+
it('fehlendes Feld → 400', async () => {
const { closing: _closing, ...rest } = valid;
await expect(messagesOf(rest)).resolves.not.toHaveLength(0);
diff --git a/apps/api/src/user/welcome-mail-template.controller.ts b/apps/api/src/user/welcome-mail-template.controller.ts
index e05df20..1ebf13e 100644
--- a/apps/api/src/user/welcome-mail-template.controller.ts
+++ b/apps/api/src/user/welcome-mail-template.controller.ts
@@ -20,9 +20,16 @@ import {
type WelcomeMailTemplateState,
} from './welcome-mail-template.service';
-/** Nur die vier Texte aus dem DTO — nie weitere Felder in die Vorlage. */
+/** Nur die sechs Texte aus dem DTO — nie weitere Felder in die Vorlage. */
function textsOf(dto: WelcomeMailTemplateDto) {
- return { subject: dto.subject, heading: dto.heading, intro: dto.intro, closing: dto.closing };
+ return {
+ subject: dto.subject,
+ heading: dto.heading,
+ intro: dto.intro,
+ loginHintDirectory: dto.loginHintDirectory,
+ loginHintLocal: dto.loginHintLocal,
+ closing: dto.closing,
+ };
}
/**
diff --git a/apps/api/src/user/welcome-mail-template.service.spec.ts b/apps/api/src/user/welcome-mail-template.service.spec.ts
index f20c7ae..ef4ef3b 100644
--- a/apps/api/src/user/welcome-mail-template.service.spec.ts
+++ b/apps/api/src/user/welcome-mail-template.service.spec.ts
@@ -18,6 +18,8 @@ type Row = {
subject: string;
heading: string;
intro: string;
+ loginHintDirectory?: string | null;
+ loginHintLocal?: string | null;
closing: string;
updatedBy: string | null;
updatedAt: Date;
@@ -66,7 +68,14 @@ function makePrisma(rows: Row[] = []) {
};
}
-const texts = { subject: 'S {{name}}', heading: 'H', intro: 'I', closing: 'C' };
+const texts = {
+ subject: 'S {{name}}',
+ heading: 'H',
+ intro: 'I',
+ loginHintDirectory: 'LD {{benutzername}}',
+ loginHintLocal: 'LL',
+ closing: 'C',
+};
let prisma: ReturnType;
let service: WelcomeMailTemplateService;
@@ -98,6 +107,25 @@ describe('WelcomeMailTemplateService', () => {
expect(await service.getCustomTexts('t1')).toEqual(texts);
});
+ it('aeltere Vorlage ohne Anmeldehinweise (NULL) → Standardtexte fuer diese Felder, Rest bleibt eigen', async () => {
+ prisma.__rows.push({
+ tenantId: 't1',
+ subject: 'Alt',
+ heading: 'H',
+ intro: 'I',
+ loginHintDirectory: null,
+ loginHintLocal: null,
+ closing: 'C',
+ updatedBy: null,
+ updatedAt: new Date(),
+ });
+ const state = await service.getState('t1');
+ expect(state.custom).toBe(true);
+ expect(state.texts.subject).toBe('Alt');
+ expect(state.texts.loginHintDirectory).toBe(DEFAULT_WELCOME_MAIL_TEXTS.loginHintDirectory);
+ expect(state.texts.loginHintLocal).toBe(DEFAULT_WELCOME_MAIL_TEXTS.loginHintLocal);
+ });
+
it('Mandantenbindung: Vorlage von t1 ist fuer t2 nicht sichtbar', async () => {
await service.save('t1', texts, 'ada');
expect(await service.getCustomTexts('t2')).toBeNull();
diff --git a/apps/api/src/user/welcome-mail-template.service.ts b/apps/api/src/user/welcome-mail-template.service.ts
index 4b89951..147ef7b 100644
--- a/apps/api/src/user/welcome-mail-template.service.ts
+++ b/apps/api/src/user/welcome-mail-template.service.ts
@@ -8,10 +8,12 @@ import { forTenant } from '../prisma/prisma-tenant.extension';
* (Administrator → Willkommensmail).
*
* Hoechstens eine Vorlage je Mandant (Tabelle "WelcomeMailTemplate",
- * `tenantId` eindeutig). Gespeichert werden nur die vier Texte; Kopf,
- * Zugangsdaten, Anmeldehinweis, Knoepfe und Fusszeile bleiben fest in
+ * `tenantId` eindeutig). Gespeichert werden nur die sechs Texte; Kopf,
+ * Zugangsdaten, Knoepfe und Fusszeile bleiben fest in
* `welcome-mail.template.ts`. Fehlt die Vorlage, gelten
- * `DEFAULT_WELCOME_MAIL_TEXTS` aus `@tessera/shared`.
+ * `DEFAULT_WELCOME_MAIL_TEXTS` aus `@tessera/shared`. Die zwei
+ * Anmeldehinweise kamen spaeter hinzu (Spalten nullable): fehlt ein Wert in
+ * einer aelteren Vorlage, gilt fuer ihn der Standardtext.
*
* Mandantenbindung: jeder Zugriff laeuft ueber `forTenant(prisma, tenantId)`.
* Beim Versand ist `tenantId` der Mandant des ZIEL-Benutzers
@@ -36,6 +38,8 @@ const TEXT_SELECT = {
subject: true,
heading: true,
intro: true,
+ loginHintDirectory: true,
+ loginHintLocal: true,
closing: true,
updatedAt: true,
updatedBy: true,
@@ -57,7 +61,16 @@ export class WelcomeMailTemplateService {
}
return {
custom: true,
- texts: { subject: row.subject, heading: row.heading, intro: row.intro, closing: row.closing },
+ texts: {
+ subject: row.subject,
+ heading: row.heading,
+ intro: row.intro,
+ loginHintDirectory: row.loginHintDirectory?.trim()
+ ? row.loginHintDirectory
+ : defaults.loginHintDirectory,
+ loginHintLocal: row.loginHintLocal?.trim() ? row.loginHintLocal : defaults.loginHintLocal,
+ closing: row.closing,
+ },
defaults,
updatedAt: row.updatedAt,
updatedBy: row.updatedBy,
@@ -90,6 +103,8 @@ export class WelcomeMailTemplateService {
subject: texts.subject,
heading: texts.heading,
intro: texts.intro,
+ loginHintDirectory: texts.loginHintDirectory,
+ loginHintLocal: texts.loginHintLocal,
closing: texts.closing,
updatedBy,
};
diff --git a/apps/api/src/user/welcome-mail.service.spec.ts b/apps/api/src/user/welcome-mail.service.spec.ts
index caae7c3..f7180a8 100644
--- a/apps/api/src/user/welcome-mail.service.spec.ts
+++ b/apps/api/src/user/welcome-mail.service.spec.ts
@@ -1,5 +1,5 @@
import { BadGatewayException, BadRequestException, ConflictException } from '@nestjs/common';
-import type { WelcomeMailTexts } from '@tessera/shared';
+import { DEFAULT_WELCOME_MAIL_TEXTS, type WelcomeMailTexts } from '@tessera/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
normalizeOrigin,
@@ -265,6 +265,8 @@ describe('WelcomeMailService.send — eigene Vorlage', () => {
subject: 'Hallo {{vorname}} bei {{firma}}',
heading: 'Schön, dass Sie da sind, {{name}}!',
intro: 'Erste Zeile\nzweite Zeile\n\nIhr Konto: {{benutzername}} / {{email}} / {{adresse}}',
+ loginHintDirectory: 'Bitte mit dem Kennwort von {{firma}} anmelden, {{vorname}}.',
+ loginHintLocal: 'Zuerst ein eigenes Passwort vergeben, {{vorname}}.',
closing: 'Grüße vom IT-Team',
};
@@ -297,6 +299,19 @@ describe('WelcomeMailService.send — eigene Vorlage', () => {
expect(rendered.html).toContain('Zu Tessera');
expect(rendered.html).toContain('Benutzername');
expect(rendered.text).toContain('Erste Zeile\nzweite Zeile');
+ // Anmeldehinweis fuer lokale Konten aus der Vorlage
+ expect(rendered.html).toContain('Zuerst ein eigenes Passwort vergeben, Max.');
+ expect(rendered.html).not.toContain('persönliches Passwort fest');
+ });
+
+ it('Verzeichniskonto mit eigener Vorlage: eigener Verzeichnis-Hinweis statt Windows-Standard', async () => {
+ const { service, mail } = make(undefined, undefined, makeTemplates({ t1: custom }));
+ await service.send(ldapUser);
+ const rendered = (mail.sendWelcomeMail.mock.calls[0] as any[])[2];
+ expect(rendered.html).toContain('Bitte mit dem Kennwort von Firma t1 anmelden');
+ expect(rendered.html).not.toContain('gewohnten Windows-Passwort');
+ expect(rendered.html).not.toContain('Zuerst ein eigenes Passwort vergeben');
+ expect(rendered.text).toContain('Bitte mit dem Kennwort von Firma t1 anmelden');
});
it('Vorlage eines ANDEREN Mandanten wirkt nicht', async () => {
@@ -315,11 +330,15 @@ describe('WelcomeMailService.send — eigene Vorlage', () => {
});
});
+const DEFAULT_TEXTS: WelcomeMailTexts = { ...DEFAULT_WELCOME_MAIL_TEXTS };
+
describe('WelcomeMailService.preview', () => {
const texts: WelcomeMailTexts = {
subject: 'Betreff {{name}}',
heading: 'Hallo {{vorname}}',
intro: 'Bei {{firma}}',
+ loginHintDirectory: 'Verzeichnis-Hinweis {{benutzername}}',
+ loginHintLocal: 'Lokal-Hinweis {{benutzername}}',
closing: '',
};
@@ -331,6 +350,8 @@ describe('WelcomeMailService.preview', () => {
expect(rendered.html).toContain('Hallo Max');
expect(rendered.html).toContain('Bei Firma t1');
expect(rendered.html).toContain('/reset-password/beispiel');
+ expect(rendered.html).toContain('Lokal-Hinweis max.mustermann');
+ expect(rendered.html).not.toContain('Verzeichnis-Hinweis');
expect(rendered.html).not.toContain('cid:');
expect(prisma.__log).toHaveLength(0);
expect(mail.sendWelcomeMail).not.toHaveBeenCalled();
@@ -338,9 +359,12 @@ describe('WelcomeMailService.preview', () => {
it('Verzeichniskonto-Variante: Windows-Hinweis statt Passwort-Knopf', async () => {
const { service } = make();
- const rendered = await service.preview('t1', texts, 'directory');
+ const rendered = await service.preview('t1', DEFAULT_TEXTS, 'directory');
expect(rendered.html).toContain('gewohnten Windows-Passwort');
expect(rendered.html).not.toContain('Passwort festlegen');
+ const custom = await service.preview('t1', texts, 'directory');
+ expect(custom.html).toContain('Verzeichnis-Hinweis max.mustermann');
+ expect(custom.html).not.toContain('Lokal-Hinweis');
});
});
@@ -349,6 +373,8 @@ describe('WelcomeMailService.sendTest', () => {
subject: 'Test {{name}}',
heading: 'Hallo {{vorname}}',
intro: 'Text',
+ loginHintDirectory: DEFAULT_WELCOME_MAIL_TEXTS.loginHintDirectory,
+ loginHintLocal: 'Eigener Lokal-Hinweis für {{vorname}}',
closing: 'Gruß',
};
@@ -371,6 +397,8 @@ describe('WelcomeMailService.sendTest', () => {
expect(rendered.html).toContain('Passwort festlegen');
expect(rendered.html).toContain('Beispiel-Link');
expect(rendered.html).not.toContain('reset-password');
+ // Anmeldehinweis fuer lokale Konten aus dem Formularinhalt
+ expect(rendered.html).toContain('Eigener Lokal-Hinweis für Ada');
expect(rendered.text).not.toContain('reset-password');
});
diff --git a/apps/web/src/components/admin/welcome-mail-template-editor.test.tsx b/apps/web/src/components/admin/welcome-mail-template-editor.test.tsx
index add42a4..b32c846 100644
--- a/apps/web/src/components/admin/welcome-mail-template-editor.test.tsx
+++ b/apps/web/src/components/admin/welcome-mail-template-editor.test.tsx
@@ -41,6 +41,8 @@ const custom = {
subject: 'Hallo {{vorname}}',
heading: 'Willkommen, {{name}}!',
intro: 'Eigene Einleitung',
+ loginHintDirectory: 'Eigener Verzeichnis-Hinweis',
+ loginHintLocal: 'Eigener Lokal-Hinweis',
closing: 'Ihr IT-Team',
};
@@ -147,7 +149,76 @@ describe('WelcomeMailTemplateEditor', () => {
).toBe(true);
});
- it('Speichern schickt die vier Texte und meldet Erfolg; Status zeigt die eigene Vorlage', async () => {
+ it('Anmeldehinweise: beide Felder nach dem Einleitungstext, mit Standardtext bzw. eigener Vorlage vorbelegt', async () => {
+ await renderLoaded();
+ const dir = screen.getByLabelText(
+ 'Anmeldehinweis für Verzeichniskonten',
+ ) as HTMLTextAreaElement;
+ const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement;
+ expect(dir.value).toBe(
+ 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.',
+ );
+ expect(local.value).toBe(
+ 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.',
+ );
+ // Reihenfolge: Einleitung → Verzeichnis → lokal → Abschluss
+ const intro = screen.getByLabelText('Einleitungstext');
+ const closing = screen.getByLabelText('Abschlusstext');
+ const follows = (a: Element, b: Element) =>
+ Boolean(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
+ expect(follows(intro, dir)).toBe(true);
+ expect(follows(dir, local)).toBe(true);
+ expect(follows(local, closing)).toBe(true);
+ expect(screen.getByText(/direkt vor dem Knopf „Passwort festlegen“/)).toBeTruthy();
+
+ cleanup();
+ api.fetchWelcomeMailTemplate.mockResolvedValue(state(true));
+ await renderLoaded();
+ expect(
+ (screen.getByLabelText('Anmeldehinweis für Verzeichniskonten') as HTMLTextAreaElement).value,
+ ).toBe(custom.loginHintDirectory);
+ expect(
+ (screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement).value,
+ ).toBe(custom.loginHintLocal);
+ });
+
+ it('Anmeldehinweis: Pflichtfeld, Platzhalter-Klick fuegt ein, Fokus schaltet die Vorschau auf die Kontoart', async () => {
+ await renderLoaded();
+ const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement;
+ fireEvent.change(local, { target: { value: 'Hallo ' } });
+ local.focus();
+ local.setSelectionRange(6, 6);
+ fireEvent.click(screen.getByRole('button', { name: '{{vorname}} einfügen' }));
+ expect(local.value).toBe('Hallo {{vorname}}');
+ expect(screen.getByRole('button', { name: 'Lokales Konto' }).getAttribute('aria-pressed')).toBe(
+ 'true',
+ );
+
+ fireEvent.change(local, { target: { value: ' ' } });
+ expect(await screen.findByText('Bitte füllen Sie dieses Feld aus.')).toBeTruthy();
+ expect((screen.getByRole('button', { name: 'Speichern' }) as HTMLButtonElement).disabled).toBe(
+ true,
+ );
+ });
+
+ it('Speichern schickt alle sechs Texte inkl. Anmeldehinweise', async () => {
+ const user = userEvent.setup();
+ await renderLoaded();
+ fireEvent.change(screen.getByLabelText('Anmeldehinweis für Verzeichniskonten'), {
+ target: { value: 'Mit dem Firmenkennwort anmelden, {{vorname}}.' },
+ });
+ fireEvent.change(screen.getByLabelText('Anmeldehinweis für lokale Konten'), {
+ target: { value: 'Erst Passwort setzen.' },
+ });
+ await user.click(screen.getByRole('button', { name: 'Speichern' }));
+ expect(api.saveWelcomeMailTemplate).toHaveBeenCalledWith({
+ ...defaults,
+ loginHintDirectory: 'Mit dem Firmenkennwort anmelden, {{vorname}}.',
+ loginHintLocal: 'Erst Passwort setzen.',
+ });
+ });
+
+ it('Speichern schickt die Texte und meldet Erfolg; Status zeigt die eigene Vorlage', async () => {
const user = userEvent.setup();
const subject = await renderLoaded();
fireEvent.change(subject, { target: { value: 'Neu für {{firma}}' } });
diff --git a/apps/web/src/components/admin/welcome-mail-template-editor.tsx b/apps/web/src/components/admin/welcome-mail-template-editor.tsx
index 71d9c5e..40caf13 100644
--- a/apps/web/src/components/admin/welcome-mail-template-editor.tsx
+++ b/apps/web/src/components/admin/welcome-mail-template-editor.tsx
@@ -24,13 +24,41 @@ type Field = keyof WelcomeMailTexts;
type PreviewAccount = 'directory' | 'local';
type Feedback = { type: 'success' | 'error'; message: string } | null;
-const FIELDS: Field[] = ['subject', 'heading', 'intro', 'closing'];
+const FIELDS: Field[] = [
+ 'subject',
+ 'heading',
+ 'intro',
+ 'loginHintDirectory',
+ 'loginHintLocal',
+ 'closing',
+];
const MULTILINE: Record = {
subject: false,
heading: false,
intro: true,
+ loginHintDirectory: true,
+ loginHintLocal: true,
closing: true,
};
+/** Pflichtfelder (dieselben wie im DTO der API). */
+const REQUIRED: ReadonlySet = new Set([
+ 'subject',
+ 'heading',
+ 'loginHintDirectory',
+ 'loginHintLocal',
+]);
+/** Zeilen der mehrzeiligen Felder. */
+const ROWS: Partial> = {
+ intro: 6,
+ loginHintDirectory: 3,
+ loginHintLocal: 3,
+ closing: 5,
+};
+/** Welche Kontoart die Vorschau zeigt, wenn ein Anmeldehinweis bearbeitet wird. */
+const HINT_ACCOUNT: Partial> = {
+ loginHintDirectory: 'directory',
+ loginHintLocal: 'local',
+};
/** Verzoegerung der Live-Vorschau nach der letzten Eingabe. */
const PREVIEW_DELAY_MS = 400;
@@ -38,11 +66,13 @@ const PREVIEW_DELAY_MS = 400;
/**
* Administrator → Willkommensmail: eigene Vorlage je Mandant.
*
- * Vier Textfelder (Betreff, Ueberschrift, Einleitung, Abschluss), eine
+ * Sechs Textfelder (Betreff, Ueberschrift, Einleitung, Anmeldehinweis fuer
+ * Verzeichniskonten, Anmeldehinweis fuer lokale Konten, Abschluss), eine
* Hinweisbox mit den Platzhaltern (Klick fuegt an der Cursorposition des
* zuletzt benutzten Feldes ein), eine Live-Vorschau (von der API gerendert,
* also dieselbe Mail wie im Versand) mit Umschalter Verzeichniskonto/lokales
- * Konto, "Testmail an mich senden" und "Auf Standard zuruecksetzen".
+ * Konto (springt beim Bearbeiten eines Anmeldehinweises auf die passende
+ * Kontoart), "Testmail an mich senden" und "Auf Standard zuruecksetzen".
*
* Unbekannte Platzhalter benennt das Formular schon beim Tippen; die API
* lehnt sie beim Speichern ohnehin mit 400 ab (Meldung wird angezeigt).
@@ -81,7 +111,7 @@ export function WelcomeMailTemplateEditor() {
// ── Pruefung im Formular (dieselben Regeln wie die API) ───────────────
const fieldErrors = (field: Field, value: string): string[] => {
const errors: string[] = [];
- if ((field === 'subject' || field === 'heading') && !value.trim()) {
+ if (REQUIRED.has(field) && !value.trim()) {
errors.push(t('errors.required'));
}
if (value.length > WELCOME_MAIL_LIMITS[field]) {
@@ -266,6 +296,8 @@ export function WelcomeMailTemplateEditor() {
'aria-describedby': describedBy,
onFocus: () => {
lastFocused.current = field;
+ const hintAccount = HINT_ACCOUNT[field];
+ if (hintAccount) setAccount(hintAccount);
},
onChange: (e: React.ChangeEvent) => {
const value = e.target.value;
@@ -283,7 +315,7 @@ export function WelcomeMailTemplateEditor() {
ref={(el) => {
fieldRefs.current[field] = el;
}}
- rows={field === 'intro' ? 6 : 5}
+ rows={ROWS[field] ?? 5}
className={`${inputClass} py-2`}
/>
) : (
diff --git a/apps/web/src/lib/welcome-mail-template-api.ts b/apps/web/src/lib/welcome-mail-template-api.ts
index 83906a6..1135321 100644
--- a/apps/web/src/lib/welcome-mail-template-api.ts
+++ b/apps/web/src/lib/welcome-mail-template-api.ts
@@ -56,12 +56,14 @@ async function request(path: string, init: RequestInit = {}): Promise {
return (await res.json()) as T;
}
-/** Nur die vier Texte senden — nie weitere Felder aus dem Formularzustand. */
+/** Nur die sechs Texte senden — nie weitere Felder aus dem Formularzustand. */
function textsOf(texts: WelcomeMailTexts): WelcomeMailTexts {
return {
subject: texts.subject,
heading: texts.heading,
intro: texts.intro,
+ loginHintDirectory: texts.loginHintDirectory,
+ loginHintLocal: texts.loginHintLocal,
closing: texts.closing,
};
}
diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json
index 1dd5f54..92d3993 100644
--- a/apps/web/src/messages/de.json
+++ b/apps/web/src/messages/de.json
@@ -746,7 +746,7 @@
"loading": "Vorlage wird geladen …",
"fixed": {
"title": "Was Sie anpassen können – und was immer drin bleibt",
- "body": "Sie bestimmen Betreff, Überschrift, Einleitung und Abschlusstext. Alles, was für die Anmeldung wichtig ist, fügt Tessera immer automatisch ein und lässt sich nicht verändern: der Kopf mit Logo, der Kasten mit Adresse und Benutzername, der Anmeldehinweis samt Knopf „Passwort festlegen“ bei lokalen Konten, der Knopf „Zu Tessera“ und die Fußzeile. So kann eine eigene Vorlage die Anmeldung nicht kaputt machen."
+ "body": "Sie bestimmen Betreff, Überschrift, Einleitung, Abschlusstext und den Anmeldehinweis – getrennt für Verzeichniskonten und für lokale Konten. Alles, was für die Anmeldung technisch nötig ist, fügt Tessera immer automatisch ein und lässt sich nicht verändern: der Kopf mit Logo, der Kasten mit Adresse und Benutzername, der Knopf „Passwort festlegen“ samt Link und Gültigkeitshinweis bei lokalen Konten, der Knopf „Zu Tessera“ und die Fußzeile. So kann eine eigene Vorlage die Anmeldung nicht kaputt machen."
},
"statusDefault": "Zurzeit gelten die Standardtexte.",
"statusCustom": "Eigene Vorlage aktiv – zuletzt geändert am {date}.",
@@ -759,6 +759,10 @@
"headingHelp": "Große Zeile oben in der Mail, Pflichtfeld.",
"intro": "Einleitungstext",
"introHelp": "Steht unter der Überschrift. Eine Leerzeile beginnt einen neuen Absatz.",
+ "loginHintDirectory": "Anmeldehinweis für Verzeichniskonten",
+ "loginHintDirectoryHelp": "Erscheint unter dem Kasten mit den Zugangsdaten, wenn der Benutzer aus dem Verzeichnis (Active Directory/LDAP) kommt und sich mit seinem gewohnten Windows-Passwort anmeldet. Pflichtfeld.",
+ "loginHintLocal": "Anmeldehinweis für lokale Konten",
+ "loginHintLocalHelp": "Erscheint bei lokalen Konten direkt vor dem Knopf „Passwort festlegen“. Knopf und Gültigkeitshinweis darunter fügt Tessera immer selbst ein. Pflichtfeld.",
"closing": "Abschlusstext",
"closingHelp": "Steht am Ende der Mail, z. B. Tipp und Grußzeile. Darf leer bleiben."
},
diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json
index 2050826..82e7ea3 100644
--- a/apps/web/src/messages/en.json
+++ b/apps/web/src/messages/en.json
@@ -746,7 +746,7 @@
"loading": "Loading template …",
"fixed": {
"title": "What you can change – and what always stays",
- "body": "You decide the subject, heading, introduction and closing text. Everything needed to sign in is always added automatically and cannot be changed: the header with logo, the box with address and username, the sign-in hint including the “Set password” button for local accounts, the “Go to Tessera” button and the footer. This way a custom template cannot break sign-in."
+ "body": "You decide the subject, heading, introduction, closing text and the sign-in hint – separately for directory accounts and for local accounts. Everything technically needed to sign in is always added automatically and cannot be changed: the header with logo, the box with address and username, the “Set password” button with its link and validity note for local accounts, the “Go to Tessera” button and the footer. This way a custom template cannot break sign-in."
},
"statusDefault": "The default texts are currently in use.",
"statusCustom": "Custom template active – last changed on {date}.",
@@ -759,6 +759,10 @@
"headingHelp": "Large line at the top of the email, required.",
"intro": "Introduction",
"introHelp": "Shown below the heading. An empty line starts a new paragraph.",
+ "loginHintDirectory": "Sign-in hint for directory accounts",
+ "loginHintDirectoryHelp": "Shown below the box with the sign-in details when the user comes from the directory (Active Directory/LDAP) and signs in with their usual Windows password. Required.",
+ "loginHintLocal": "Sign-in hint for local accounts",
+ "loginHintLocalHelp": "Shown for local accounts right before the “Set password” button. Tessera always adds the button and the validity note below it. Required.",
"closing": "Closing text",
"closingHelp": "Shown at the end of the email, e.g. a tip and a sign-off. May be left empty."
},
diff --git a/docs/anleitung-administration.md b/docs/anleitung-administration.md
index d3c174f..5d16898 100644
--- a/docs/anleitung-administration.md
+++ b/docs/anleitung-administration.md
@@ -64,16 +64,18 @@ Ein Passwort steht nie in der Mail. Bei einem lokalen Konto, das bereits ein Pas
#### Eigene Vorlage für die Willkommensmail
-Unter **Administrator → Willkommensmail** legen Sie fest, mit welchen Worten Tessera neue Benutzer begrüßt. Die Vorlage gilt für Ihren Mandanten; ADMIN und SUPER_ADMIN können sie bearbeiten. Anpassen lassen sich vier Texte:
+Unter **Administrator → Willkommensmail** legen Sie fest, mit welchen Worten Tessera neue Benutzer begrüßt. Die Vorlage gilt für Ihren Mandanten; ADMIN und SUPER_ADMIN können sie bearbeiten. Anpassen lassen sich sechs Texte:
- **Betreff** (Pflicht, höchstens 200 Zeichen),
- **Überschrift** (Pflicht, höchstens 200 Zeichen),
- **Einleitungstext** (höchstens 4000 Zeichen; eine Leerzeile beginnt einen neuen Absatz),
+- **Anmeldehinweis für Verzeichniskonten** (Pflicht, höchstens 1000 Zeichen) – erscheint unter dem Kasten mit den Zugangsdaten, wenn der Benutzer aus dem Verzeichnis (AD/LDAP) kommt; Standard: „Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.“,
+- **Anmeldehinweis für lokale Konten** (Pflicht, höchstens 1000 Zeichen) – erscheint bei lokalen Konten direkt vor dem Button „Passwort festlegen“; Standard: „Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.“,
- **Abschlusstext** (höchstens 4000 Zeichen, darf leer bleiben – etwa ein Tipp und die Grußzeile).
-Alles, was für die Anmeldung wichtig ist, fügt Tessera immer automatisch ein und lässt sich nicht verändern: der Kopf mit Logo, der Kasten mit Adresse und Benutzername, der Anmeldehinweis samt Button „Passwort festlegen“ bei lokalen Konten, der Button „Zu Tessera“ und die Fußzeile. So kann eine eigene Vorlage die Anmeldung nicht kaputt machen. Die Texte sind reiner Text – Formatierungen oder HTML werden nicht übernommen, sondern so angezeigt, wie sie eingegeben wurden.
+Alles, was für die Anmeldung technisch nötig ist, fügt Tessera immer automatisch ein und lässt sich nicht verändern: der Kopf mit Logo, der Kasten mit Adresse und Benutzername, der Button „Passwort festlegen“ samt persönlichem Link und Gültigkeitshinweis bei lokalen Konten, der Button „Zu Tessera“ und die Fußzeile. Beide Anmeldehinweise dürfen Leerzeilen (neuer Absatz) enthalten. Vorlagen, die vor Einführung der Anmeldehinweise gespeichert wurden, verwenden dafür automatisch die Standardtexte. So kann eine eigene Vorlage die Anmeldung nicht kaputt machen. Die Texte sind reiner Text – Formatierungen oder HTML werden nicht übernommen, sondern so angezeigt, wie sie eingegeben wurden.
-**Platzhalter** funktionieren in allen vier Feldern und werden beim Versand durch die Angaben des jeweiligen Benutzers ersetzt. Ein Klick auf einen Platzhalter in der Hinweisbox fügt ihn an der Cursorposition des zuletzt benutzten Feldes ein.
+**Platzhalter** funktionieren in allen sechs Feldern und werden beim Versand durch die Angaben des jeweiligen Benutzers ersetzt. Ein Klick auf einen Platzhalter in der Hinweisbox fügt ihn an der Cursorposition des zuletzt benutzten Feldes ein.
| Platzhalter | Bedeutung | Beispiel |
|---|---|---|
@@ -86,7 +88,7 @@ Alles, was für die Anmeldung wichtig ist, fügt Tessera immer automatisch ein u
Andere Platzhalter kennt Tessera nicht: die Seite nennt sie schon beim Tippen („Unbekannter Platzhalter {{xyz}}“), und das Speichern wird abgelehnt.
-Rechts zeigt eine **Vorschau** die Mail genau so, wie sie verschickt wird – mit Beispielwerten und umschaltbar zwischen „Verzeichniskonto“ und „Lokales Konto“. **Testmail an mich senden** schickt den aktuellen Inhalt des Formulars (auch ungespeichert) an Ihre eigene E-Mail-Adresse, mit Ihrem Konto als Beispiel. Die Testmail enthält nie einen echten Link zum Festlegen eines Passworts: bei einem lokalen Konto führt der Button nur zur Anmeldeseite und ist als Beispiel gekennzeichnet. **Auf Standard zurücksetzen** löscht die eigene Vorlage nach einer Rückfrage; danach gelten wieder die mitgelieferten Standardtexte. Ohne eigene Vorlage verwendet Tessera immer die Standardtexte.
+Rechts zeigt eine **Vorschau** die Mail genau so, wie sie verschickt wird – mit Beispielwerten und umschaltbar zwischen „Verzeichniskonto“ und „Lokales Konto“; die Vorschau zeigt dabei den jeweiligen Anmeldehinweis und springt beim Bearbeiten eines Anmeldehinweises von selbst auf die passende Kontoart. **Testmail an mich senden** schickt den aktuellen Inhalt des Formulars (auch ungespeichert) an Ihre eigene E-Mail-Adresse, mit Ihrem Konto als Beispiel. Die Testmail enthält nie einen echten Link zum Festlegen eines Passworts: bei einem lokalen Konto führt der Button nur zur Anmeldeseite und ist als Beispiel gekennzeichnet. **Auf Standard zurücksetzen** löscht die eigene Vorlage nach einer Rückfrage; danach gelten wieder die mitgelieferten Standardtexte. Ohne eigene Vorlage verwendet Tessera immer die Standardtexte.
### Deaktivieren und Löschen
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index b436fe7..bd137ae 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -293,15 +293,16 @@ export type CustomModuleCategory = (typeof CUSTOM_MODULE_CATEGORIES)[number];
* Eigene Vorlage der Willkommensmail (Administrator → Willkommensmail).
*
* Eine Stelle fuer API und Oberflaeche: welche Platzhalter es gibt, wie lang
- * die vier Textfelder sein duerfen, wie die Standardtexte lauten (Vorbelegung
+ * die sechs Textfelder sein duerfen, wie die Standardtexte lauten (Vorbelegung
* im Formular, "Auf Standard zuruecksetzen", Versand ohne eigene Vorlage) und
* wie unbekannte Platzhalter erkannt werden. Die API lehnt eine Vorlage mit
* unbekanntem Platzhalter mit 400 ab; das Formular benennt ihn schon vorher.
*
- * Die festen Bausteine der Mail (Kopf, Kasten Adresse/Benutzername,
- * Anmeldehinweis mit "Passwort festlegen", Knopf "Zu Tessera", Fusszeile)
- * sind KEIN Teil der Vorlage — so kann eine eigene Vorlage die Anmeldung
- * nicht kaputt machen.
+ * Die festen Bausteine der Mail (Kopf, Kasten Adresse/Benutzername, Knopf
+ * "Passwort festlegen" samt Gueltigkeitshinweis, Knopf "Zu Tessera",
+ * Fusszeile) sind KEIN Teil der Vorlage — so kann eine eigene Vorlage die
+ * Anmeldung nicht kaputt machen. Anpassbar ist nur der Anmeldehinweis-TEXT
+ * davor, getrennt fuer Verzeichniskonten und lokale Konten.
*/
export const WELCOME_MAIL_PLACEHOLDERS = [
'name',
@@ -314,11 +315,15 @@ export const WELCOME_MAIL_PLACEHOLDERS = [
export type WelcomeMailPlaceholder = (typeof WELCOME_MAIL_PLACEHOLDERS)[number];
-/** Die vier bearbeitbaren Texte der Willkommensmail. */
+/** Die sechs bearbeitbaren Texte der Willkommensmail. */
export interface WelcomeMailTexts {
subject: string;
heading: string;
intro: string;
+ /** Anmeldehinweis fuer verzeichnisgefuehrte Konten (Windows-Passwort). */
+ loginHintDirectory: string;
+ /** Anmeldehinweis fuer lokale Konten, steht vor dem Knopf "Passwort festlegen". */
+ loginHintLocal: string;
closing: string;
}
@@ -327,6 +332,8 @@ export const WELCOME_MAIL_LIMITS: Readonly = {
heading: 'Willkommen bei Tessera, {{name}}!',
intro:
'Für Sie wurde ein Zugang zu Tessera eingerichtet – Ihrer zentralen Plattform für Werkzeuge und Abläufe im Unternehmen. Alles, was Sie für Ihre tägliche Arbeit brauchen, finden Sie dort an einem Ort.',
+ loginHintDirectory:
+ 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.',
+ loginHintLocal:
+ 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.',
closing:
'Tipp: Tessera gibt es auch als Desktop-App – den Download finden Sie auf der Anmeldeseite.\n\nViel Erfolg mit Tessera!',
};