Unterschriftenservice, Extern-Modus, Adressmodell, Alt-Mail, Team-Mails

skrift-app (Konfigurator):
- Datei-Produkte (Unterschriftenservice): Mehrfach-Upload (PDF/Word/Excel),
  komplett preisfrei (keine Preisleiste/Aufpreise/Kostenübersicht/Zahlung)
- Extern-/Etsy-Modus (?extern=1&versand&kuvert&beschriftung&menge): vom Betreiber
  vorkonfiguriert, Kunde füllt nur Empfänger/Text/Rechnungsdaten/Übersicht,
  keine Preise/Zahlung; Menge greift immer (auch >25 / ohne Empfänger); eigener
  Danke-Text
- Adressmodell: zwei feste Adressen (Rechnung/Lieferung) ohne Namen statt
  Dropdown; Auto-Vorbefüllung; Lieferadresse nur bei Sammelversand; abweichende
  Lieferadresse per Default aus; Pflichtfelder erzwungen
- Platzhalter-Tabelle immer im Text-Schritt (auch Sammelversand ohne Kuvert) +
  Excel; Tabellen-Eingabe nur bis 25, „Ab 26"-Hinweis entfernt
- Stammdaten: zwei Adressblöcke + Erfolgsmeldung

Directus/Backend:
- Signatur-Endpunkte /signature-preview + /signature-render (Proxy zum
  signature-service, Marker + Konto-Offsets), Modul-Reiter „Unterschriften"
  (aus Auftrag oder manuell), Tabelle über volle Breite
- Generierung läuft bei jeder Bestellung; Kuverts immer (Datei-Produkt: nur
  Kuvert, DIN Lang als Default); source_files am Auftrag
- Produktions-Board: „Dateien"-Button (hochgeladene Originale, keine SVGs)
- Alt-Mail-Login: Zweit-Mails melden im selben Konto an (skrift-auth)
- Team-Mails schlank + konfigurierbar (email_settings.team_email) für
  Bestellung/Muster/Anfrage; keine Bestellbestätigung bei Datei-Produkten,
  separate Vorlagen unterschriften_eingang/extern_eingang
- Rechnungsadresse wird ins Konto übernommen, wenn dort noch keine steht
- Kunden-Policy: directus_files create/read (Upload); Bootstrap: users
  billing_address/shipping_address/alt_emails, orders.source_files/source=extern,
  signatures marker/offsets, unterschriftenservice input_mode=datei/pricing_mode=formel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-07-26 14:52:09 +02:00
parent 6232cecdb1
commit 3cb93d078e
12 changed files with 778 additions and 258 deletions

View File

@@ -43,14 +43,26 @@ const handler = (router, { services, getSchema, env, logger }) => {
const ctx = async () => ({ schema: await getSchema(), accountability: null });
// Findet das Konto per primärer E-Mail ODER per zusätzlicher Login-Mail
// (alt_emails). So melden sich Zweit-Mails im SELBEN Konto an.
async function findUserByEmail(email, schema) {
const users = new UsersService({ schema, accountability: null });
const found = await users.readByQuery({
filter: { email: { _eq: email } },
fields: ['id', 'email', 'status'],
limit: 1,
});
return found?.[0] || null;
const felder = ['id', 'email', 'status', 'alt_emails'];
const primaer = await users.readByQuery({ filter: { email: { _eq: email } }, fields: felder, limit: 1 });
if (primaer?.[0]) return primaer[0];
// Zusätzliche Login-Mails: JSON-Feld ist DB-abhängig schwer filterbar, daher
// die (wenigen) Konten mit alt_emails laden und exakt in JS prüfen.
try {
const cand = await users.readByQuery({ filter: { alt_emails: { _nnull: true } }, fields: felder, limit: -1 });
const treffer = (cand || []).find((u) => {
const liste = Array.isArray(u.alt_emails)
? u.alt_emails
: (typeof u.alt_emails === 'string' ? (() => { try { return JSON.parse(u.alt_emails); } catch { return []; } })() : []);
return Array.isArray(liste) && liste.map((x) => norm(x)).includes(email);
});
if (treffer) return treffer;
} catch { /* alt_emails evtl. noch nicht vorhanden → ignorieren */ }
return null;
}
async function sendCodeMail(email, code) {
@@ -173,6 +185,11 @@ const handler = (router, { services, getSchema, env, logger }) => {
if (!user || user.status !== 'active')
return res.status(401).json({ error: 'Kein aktives Konto zu dieser Adresse.' });
// WICHTIG: Directus kennt nur die PRIMÄRE E-Mail als Login. Wurde der Code
// an eine Zweit-Mail geschickt, melden wir dennoch mit der primären an –
// dadurch landet die Zweit-Mail im selben Konto (voller Zugriff).
const loginEmail = norm(user.email);
// Offizieller Login-Pfad mit rotierendem Zufallspasswort (s. Kopfkommentar).
const users = new UsersService({ schema, accountability: null });
const tempPassword = tempPasswort();
@@ -183,9 +200,9 @@ const handler = (router, { services, getSchema, env, logger }) => {
// curl funktioniert – daher zuerst diese Variante, dann Fallbacks.
const auth = new AuthenticationService({ schema, accountability: null });
const versuche = [
() => auth.login('default', { email, password: tempPassword }),
() => auth.login({ email, password: tempPassword }),
() => auth.login({ email, password: tempPassword }, {}),
() => auth.login('default', { email: loginEmail, password: tempPassword }),
() => auth.login({ email: loginEmail, password: tempPassword }),
() => auth.login({ email: loginEmail, password: tempPassword }, {}),
];
let result, letzterFehler;
for (const versuch of versuche) {