diff --git a/backend/ldapOperations.js b/backend/ldapOperations.js index 943fff2..9a44e55 100644 --- a/backend/ldapOperations.js +++ b/backend/ldapOperations.js @@ -1,4 +1,5 @@ const { Client, Attribute, Change } = require('ldapts'); +const { transliterateUmlauts, toSamAccountName } = require('./umlauts'); /** * LDAP Operations Module (ldapts) @@ -133,12 +134,10 @@ function escapeLDAPDN(str) { /** * Replace German umlauts and ß for sAMAccountName compatibility. + * (Delegiert an zentrale umlauts.js — Logik dort gepflegt.) */ function replaceUmlauts(str) { - return str - .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue') - .replace(/ß/g, 'ss'); + return transliterateUmlauts(str); } /** @@ -166,14 +165,15 @@ async function createADUser({ ou, vorname, nachname, email, username, password, const escapedCN = escapeLDAPDN(cnValue); const dn = 'CN=' + escapedCN + ',' + ou; - // Build UPN + // Build UPN — FIX: Username umlautfrei normalisieren (ä→ae etc.), + // damit kein ungültiger UPN wie müller@... entsteht. const upnSuffix = LDAP_UPN_SUFFIX || LDAP_BIND_USER.split('@')[1] || LDAP_DOMAIN.toLowerCase() + '.intra'; - const userPrincipalName = username + '@' + upnSuffix; + const userPrincipalName = toSamAccountName(username) + '@' + upnSuffix; // sAMAccountName: max 20 chars - let sAMAccountName = username; + let sAMAccountName = toSamAccountName(username); if (vorname && nachname) { - sAMAccountName = replaceUmlauts(nachname + vorname.charAt(0)).replace(/[^a-zA-Z0-9]/g, ''); + sAMAccountName = toSamAccountName(nachname + vorname.charAt(0)); } sAMAccountName = sAMAccountName.substring(0, 20); diff --git a/backend/routes/ad.js b/backend/routes/ad.js index 7636f94..5a6add1 100644 --- a/backend/routes/ad.js +++ b/backend/routes/ad.js @@ -8,6 +8,7 @@ const { isLDAPConfigured } = require('../ldapSync'); const { browseOUTree, createADUser, checkADUserExists, deleteADUser } = require('../ldapOperations'); const { searchADGroups, addUserToGroups, browseADGroups } = require('../ldapOperations'); const { validate, createADUserSchema, deleteADUserSchema } = require('../middleware/validation'); +const { toSamAccountName } = require('../umlauts'); const router = express.Router(); @@ -68,14 +69,14 @@ router.post('/create-user', adminMiddleware, validate(createADUserSchema), async const { ou, vorname, nachname, email, username, password, department, telefon, titel, displayName, physicalDeliveryOfficeName, company, description, wWWHomePage, streetAddress, postOfficeBox, l, st, postalCode, c, groups } = req.validatedBody; try { - let sAMAccountName = username; + // FIX: Umlaute zentral transliterieren (ä→ae etc.) — galt vorher nur für + // nachname/vorname einzeln; jetzt einheitlich über umlauts.js. Der Username + // (und damit UPN) wird ebenfalls umlautfrei normalisiert. + let sAMAccountName = toSamAccountName(username); if (vorname && nachname) { - sAMAccountName = nachname.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue').replace(/ß/g, 'ss') - + vorname.charAt(0).replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue').replace(/ß/g, 'ss'); - sAMAccountName = sAMAccountName.replace(/[^a-zA-Z0-9]/g, '').substring(0, 20); + sAMAccountName = toSamAccountName(nachname + vorname.charAt(0)); } + sAMAccountName = sAMAccountName.substring(0, 20); const existing = await checkADUserExists(username, sAMAccountName); if (existing) { diff --git a/backend/umlauts.js b/backend/umlauts.js new file mode 100644 index 0000000..0558a73 --- /dev/null +++ b/backend/umlauts.js @@ -0,0 +1,49 @@ +/** + * Umlaut-Transliteration für AD-kompatible Namen (E-Mails, sAMAccountName, UPN). + * + * Wandelt deutsche Sonderzeichen in ASCII-Äquivalente um: + * ä→ae, ö→oe, ü→ue, ß→ss (und Großvarianten) + * + * WICHTIG: Reihenfolge — Großbuchstaben-Varianten VOR toLowerCase() behandeln, + * damit "Schröder" → "Schroeder" (nicht "schroder") und "Straße" → "Strasse". + */ +function transliterateUmlauts(str) { + if (typeof str !== 'string') return ''; + return str + .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') + .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue') + .replace(/ß/g, 'ss'); +} + +/** + * Normalisiert einen Namen für den lokalen Teil einer E-Mail-Adresse: + * Umlaute → ASCII, Kleinbuchstaben, Leerzeichen → Punkte, nur a-z0-9. erlaubt. + * Beispiel: "Jürgen Schröder" → "juergen.schroeder" + */ +function toEmailLocalPart(str) { + if (typeof str !== 'string') return ''; + return transliterateUmlauts(str) + .toLowerCase() + .replace(/\s+/g, '.') + .replace(/[^a-z0-9.]/g, ''); +} + +/** + * Normalisiert einen Namen für sAMAccountName/UPN (Punkt-Format): + * wie toEmailLocalPart, aber ohne Punkt-Entfernung (Punkte sind erlaubt). + */ +function toAdAccountName(str) { + return toEmailLocalPart(str); +} + +/** + * Normalisiert einen Namen für das Pre-Windows-2000-Format (Nachname+Initial): + * Umlaute → ASCII, nur a-zA-Z0-9 erlaubt, Groß-/Kleinschreibung bleibt erhalten. + * Beispiel: "Schröder" + "J" → "SchroederJ" + */ +function toSamAccountName(str) { + if (typeof str !== 'string') return ''; + return transliterateUmlauts(str).replace(/[^a-zA-Z0-9]/g, ''); +} + +module.exports = { transliterateUmlauts, toEmailLocalPart, toAdAccountName, toSamAccountName }; \ No newline at end of file diff --git a/docker-compose.stack.yml b/docker-compose.stack.yml index adeb399..5cdf9ff 100644 --- a/docker-compose.stack.yml +++ b/docker-compose.stack.yml @@ -6,7 +6,7 @@ services: app: - image: gitea.leonkn.de/lxe/workflow-app:DEV2.0-v9 + image: gitea.leonkn.de/lxe/workflow-app:DEV2.0-v10 ports: - "3900:5000" volumes: diff --git a/frontend/src/components/FillModal.jsx b/frontend/src/components/FillModal.jsx index 1e9dc2c..cac50f4 100644 --- a/frontend/src/components/FillModal.jsx +++ b/frontend/src/components/FillModal.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; import { useToast, ConfirmModal } from './Toast'; import { apiFetch } from '../utils/api'; +import { toEmailLocalPart, toSamAccountName } from '../utils/umlauts'; export default function FillModal({ template, onSubmit, onClose }) { const { user: currentUser } = useAuth(); @@ -95,7 +96,8 @@ export default function FillModal({ template, onSubmit, onClose }) { const parts = sourceFields.map((fieldLabel) => { const sourceStep = template.steps.find((s) => s.label === fieldLabel); if (!sourceStep || !values[sourceStep.id]) return ''; - return values[sourceStep.id].trim().toLowerCase().replace(/\s+/g, '.'); + // FIX: Umlaute konvertieren (ä→ae etc.) — sonst ungültige E-Mails wie j.schröder@... + return toEmailLocalPart(values[sourceStep.id]); }).filter(Boolean); if (parts.length === 0) return ''; const domain = step.email_domain.startsWith('@') ? step.email_domain : '@' + step.email_domain; @@ -151,13 +153,7 @@ export default function FillModal({ template, onSubmit, onClose }) { return sourceStep ? (values[sourceStep.id] || '').trim() : ''; }).filter(Boolean); if (sourceValues.length > 0) { - value = sourceValues.join('.') - .toLowerCase() - .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'ae').replace(/Ö/g, 'oe').replace(/Ü/g, 'ue') - .replace(/ß/g, 'ss') - .replace(/\s+/g, '.') - .replace(/[^a-zA-Z0-9.]/g, ''); + value = toEmailLocalPart(sourceValues.join('.')); } else { value = ''; } @@ -168,11 +164,7 @@ export default function FillModal({ template, onSubmit, onClose }) { const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : ''; const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : ''; if (vorname && nachname) { - value = (nachname + vorname.charAt(0)) - .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue') - .replace(/ß/g, 'ss') - .replace(/[^a-zA-Z0-9]/g, ''); + value = toSamAccountName(nachname + vorname.charAt(0)); } else { value = values[s.id] || ''; } @@ -337,13 +329,7 @@ export default function FillModal({ template, onSubmit, onClose }) { return sourceStep ? (values[sourceStep.id] || '').trim() : ''; }).filter(Boolean); if (sourceValues.length > 0) { - return sourceValues.join('.') - .toLowerCase() - .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'ae').replace(/Ö/g, 'oe').replace(/Ü/g, 'ue') - .replace(/ß/g, 'ss') - .replace(/\s+/g, '.') - .replace(/[^a-zA-Z0-9.]/g, ''); + return toEmailLocalPart(sourceValues.join('.')); } return ''; } @@ -353,11 +339,7 @@ export default function FillModal({ template, onSubmit, onClose }) { const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : ''; const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : ''; if (vorname && nachname) { - return (nachname + vorname.charAt(0)) - .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') - .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue') - .replace(/ß/g, 'ss') - .replace(/[^a-zA-Z0-9]/g, ''); + return toSamAccountName(nachname + vorname.charAt(0)); } return ''; })()} diff --git a/frontend/src/utils/umlauts.js b/frontend/src/utils/umlauts.js new file mode 100644 index 0000000..2123f2f --- /dev/null +++ b/frontend/src/utils/umlauts.js @@ -0,0 +1,49 @@ +/** + * Umlaut-Transliteration für AD-kompatible Namen (E-Mails, sAMAccountName, UPN). + * Frontend-Variante (ES-Module) von backend/umlauts.js — Logik identisch halten! + * + * Wandelt deutsche Sonderzeichen in ASCII-Äquivalente um: + * ä→ae, ö→oe, ü→ue, ß→ss (und Großvarianten) + */ + +/** + * Umlaute → ASCII-Äquivalente. Großbuchstaben-Varianten werden erhalten + * ("Schröder" → "Schroeder", "Straße" → "Strasse"). + */ +export function transliterateUmlauts(str) { + if (typeof str !== 'string') return ''; + return str + .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue') + .replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue') + .replace(/ß/g, 'ss'); +} + +/** + * Normalisiert einen Namen für den lokalen Teil einer E-Mail-Adresse: + * Umlaute → ASCII, Kleinbuchstaben, Leerzeichen → Punkte, nur a-z0-9. erlaubt. + * Beispiel: "Jürgen Schröder" → "juergen.schroeder" + */ +export function toEmailLocalPart(str) { + if (typeof str !== 'string') return ''; + return transliterateUmlauts(str) + .toLowerCase() + .replace(/\s+/g, '.') + .replace(/[^a-z0-9.]/g, ''); +} + +/** + * Normalisiert einen Namen für sAMAccountName/UPN (Punkt-Format). + */ +export function toAdAccountName(str) { + return toEmailLocalPart(str); +} + +/** + * Normalisiert einen Namen für das Pre-Windows-2000-Format (Nachname+Initial): + * Umlaute → ASCII, nur a-zA-Z0-9, Groß-/Kleinschreibung bleibt erhalten. + * Beispiel: "Schröder" + "J" → "SchroederJ" + */ +export function toSamAccountName(str) { + if (typeof str !== 'string') return ''; + return transliterateUmlauts(str).replace(/[^a-zA-Z0-9]/g, ''); +} \ No newline at end of file