diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2207be7..17c3478 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +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.
- 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“.
@@ -16,7 +17,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
### Behoben
- Anmeldung: Wer schon angemeldet ist und die Anmeldeseite aufruft, landet jetzt direkt auf dem Dashboard.
-- Willkommensmail: Logo und Schriftzug erscheinen jetzt in jedem Mailprogramm. Bisher steckten sie in einem Bild; zeigte Outlook es nicht an, blieb nur ein großer schwarzer Kasten. Die Welle darunter ist nur noch ein schmaler Streifen.
+- Willkommensmail: Logo und Schriftzug erscheinen jetzt in jedem Mailprogramm. Bisher steckten sie in einem Bild; zeigte Outlook es nicht an, blieb nur ein großer schwarzer Kasten. Die Welle darunter ist nur noch ein schmaler Streifen; zeigt ein Mailprogramm sie nicht an (etwa Outlook im Browser), bleibt keine weiße Lücke mehr.
- Anmeldung: Eine geänderte Rolle, eine Deaktivierung oder das Löschen eines Kontos wirkt jetzt sofort. Bisher galt bis zu 30 Tage die Rolle vom Zeitpunkt der Anmeldung weiter – ein herabgestufter Administrator behielt seine Rechte, ein deaktiviertes Konto konnte mit seiner Sitzung weiterarbeiten, und die Benutzerliste ließ sich nach einer Rollenänderung nicht laden.
## 1.8.0 – 2026-09-30
diff --git a/apps/api/assets/mail/welcome-header.png b/apps/api/assets/mail/welcome-header.png
index 653892e..a4c7967 100644
Binary files a/apps/api/assets/mail/welcome-header.png and b/apps/api/assets/mail/welcome-header.png differ
diff --git a/apps/api/assets/mail/welcome-header.svg b/apps/api/assets/mail/welcome-header.svg
index c741cc7..ab817c2 100644
--- a/apps/api/assets/mail/welcome-header.svg
+++ b/apps/api/assets/mail/welcome-header.svg
@@ -1,7 +1,7 @@
-
\n`. Alles andere bleibt fest.
+ *
* E-Mail-tauglich gebaut, weil Outlook (Word-Darstellung), Gmail und Apple
* Mail sehr unterschiedlich darstellen:
* - Tabellenlayout, alle Stile inline, hoechstens 600 px breit;
@@ -25,6 +33,14 @@
* Windows-Passwort.
*/
+import {
+ DEFAULT_WELCOME_MAIL_TEXTS,
+ isWelcomeMailPlaceholder,
+ WELCOME_MAIL_PLACEHOLDER_RE,
+ type WelcomeMailPlaceholder,
+ type WelcomeMailTexts,
+} from '@tessera/shared';
+
/** Farben aus dem Design "Mosaik" (globals.css / brand.ts). */
const C = {
page: '#eceef1',
@@ -46,17 +62,31 @@ const FONT =
export type WelcomeMailAccount =
| { kind: 'directory' }
| { kind: 'local'; setPasswordUrl: string; validHours: number }
- | { kind: 'local-no-link' };
+ | { kind: 'local-no-link' }
+ /**
+ * Nur Testmail aus Administrator → Willkommensmail: derselbe Baustein wie
+ * `local`, aber OHNE Token — der Knopf fuehrt zur Anmeldeseite und ein
+ * Hinweis sagt, was er in der echten Mail tut.
+ */
+ | { kind: 'local-example' };
export interface WelcomeMailInput {
/** Anzeigename, sonst Benutzername. */
name: string;
username: string;
+ /** E-Mail-Adresse des Empfaengers (Platzhalter `{{email}}`). */
+ email: string;
+ /** Name des Mandanten (Platzhalter `{{firma}}`). */
+ tenantName: string;
/** Oeffentliche Basisadresse der Web-Oberflaeche, ohne abschliessenden Schraegstrich. */
appUrl: string;
account: WelcomeMailAccount;
- /** `cid:...` im Versand, relativer Pfad in der Vorschau, `null` = Textkopf. */
+ /** `cid:...` im Versand, `data:` in der Vorschau, `null` = Textkopf. */
headerImageSrc: string | null;
+ /** Eigene Vorlage des Mandanten; fehlt sie, gelten die Standardtexte. */
+ texts?: WelcomeMailTexts | null;
+ /** Hinweisleiste ganz oben (nur Testmail), reiner Text. */
+ notice?: string | null;
}
export interface RenderedWelcomeMail {
@@ -65,13 +95,55 @@ export interface RenderedWelcomeMail {
html: string;
}
-export const WELCOME_MAIL_SUBJECT = 'Willkommen bei Tessera';
+export const WELCOME_MAIL_SUBJECT = DEFAULT_WELCOME_MAIL_TEXTS.subject;
const FOOTER = 'Diese E-Mail wurde von Tessera im Auftrag Ihres Administrators versendet.';
-const 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.';
-const DESKTOP_HINT =
- 'Tessera gibt es auch als Desktop-App – den Download finden Sie auf der Anmeldeseite.';
+const EXAMPLE_LINK_NOTE =
+ 'Beispiel-Link: In der echten Willkommensmail führt dieser Knopf zu einem persönlichen Link, mit dem der neue Benutzer sein Passwort festlegt. In dieser Testmail öffnet er nur die Anmeldeseite.';
+
+/** Werte der Platzhalter, noch NICHT escaped (Ersetzung laeuft auf reinem Text). */
+export type WelcomeMailPlaceholderValues = Record;
+
+/** Erstes Wort des Anzeigenamens, sonst der Benutzername. */
+export function firstNameOf(displayName: string | null | undefined, username: string): string {
+ const first = (displayName ?? '').trim().split(/\s+/)[0];
+ return first || username;
+}
+
+/**
+ * Setzt die Platzhalter in einen REINEN Text ein (Gross-/Kleinschreibung
+ * egal). Unbekannte bleiben stehen — gespeicherte Vorlagen enthalten keine,
+ * das prueft die API beim Speichern. Das Ergebnis ist weiter reiner Text und
+ * wird erst beim Einbau ins HTML escaped.
+ */
+export function applyWelcomeMailPlaceholders(
+ text: string,
+ values: WelcomeMailPlaceholderValues,
+): string {
+ return text.replace(WELCOME_MAIL_PLACEHOLDER_RE, (whole, name: string) =>
+ isWelcomeMailPlaceholder(name) ? values[name.toLowerCase() as WelcomeMailPlaceholder] : whole,
+ );
+}
+
+/** Zeilenenden vereinheitlichen, Leerraum an den Raendern entfernen. */
+function normalizeText(value: string): string {
+ return value.replace(/\r\n?/g, '\n').trim();
+}
+
+/** Einzeilig (Betreff, Ueberschrift): jeder Umbruch/Leerraum-Lauf wird ein Leerzeichen. */
+function singleLine(value: string): string {
+ return value.replace(/\s+/g, ' ').trim();
+}
+
+/** Absaetze: Leerzeile trennt, einfacher Umbruch bleibt im Absatz. */
+function paragraphsOf(value: string): string[] {
+ const text = normalizeText(value);
+ if (!text) return [];
+ return text
+ .split(/\n[ \t]*\n+/)
+ .map((part) => part.trim())
+ .filter(Boolean);
+}
export function escapeHtml(value: string): string {
return value
@@ -92,6 +164,7 @@ function loginHintText(account: WelcomeMailAccount): string {
case 'directory':
return 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.';
case 'local':
+ case 'local-example':
return 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.';
case 'local-no-link':
return 'Ihr Startpasswort erhalten Sie von Ihrem Administrator.';
@@ -154,8 +227,10 @@ ${row([olive, olive, C.yellow])}${spacer}${row([null, olive, null])}${spacer}${r
* - Bildmarke (HTML-Kacheln) und Schriftzug "Tessera" (echter Text) in einer
* niedrigen dunklen Leiste — erscheinen immer;
* - darunter die Duenen-Welle als schmaler Bildstreifen (`src`, im Versand
- * `cid:`), der ins Weiss der Karte auslaeuft. Fehlt er, bleibt nur weisser
- * Abstand, kein dunkler Block.
+ * `cid:`, 600x40), der ins Weiss der Karte auslaeuft. Die Zelle um das
+ * Bild traegt die dunkle Kopffarbe (zweite Rueckmeldung des Nutzers: sein
+ * Outlook zeigt das CID-Bild nicht, vorher stand dort eine "riesengrosse
+ * weisse Luecke"): fehlt das Bild, wirkt der Kopf nur etwas hoeher.
*/
function headerRow(src: string | null): string {
const bar = `