Umlaut-Fix: zentrale Transliteration (ae/oe/ue/ss) fuer E-Mails, UPN und sAMAccountName + Release v10
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
const { Client, Attribute, Change } = require('ldapts');
|
const { Client, Attribute, Change } = require('ldapts');
|
||||||
|
const { transliterateUmlauts, toSamAccountName } = require('./umlauts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LDAP Operations Module (ldapts)
|
* LDAP Operations Module (ldapts)
|
||||||
@@ -133,12 +134,10 @@ function escapeLDAPDN(str) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace German umlauts and ß for sAMAccountName compatibility.
|
* Replace German umlauts and ß for sAMAccountName compatibility.
|
||||||
|
* (Delegiert an zentrale umlauts.js — Logik dort gepflegt.)
|
||||||
*/
|
*/
|
||||||
function replaceUmlauts(str) {
|
function replaceUmlauts(str) {
|
||||||
return str
|
return transliterateUmlauts(str);
|
||||||
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue')
|
|
||||||
.replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue')
|
|
||||||
.replace(/ß/g, 'ss');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,14 +165,15 @@ async function createADUser({ ou, vorname, nachname, email, username, password,
|
|||||||
const escapedCN = escapeLDAPDN(cnValue);
|
const escapedCN = escapeLDAPDN(cnValue);
|
||||||
const dn = 'CN=' + escapedCN + ',' + ou;
|
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 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
|
// sAMAccountName: max 20 chars
|
||||||
let sAMAccountName = username;
|
let sAMAccountName = toSamAccountName(username);
|
||||||
if (vorname && nachname) {
|
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);
|
sAMAccountName = sAMAccountName.substring(0, 20);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { isLDAPConfigured } = require('../ldapSync');
|
|||||||
const { browseOUTree, createADUser, checkADUserExists, deleteADUser } = require('../ldapOperations');
|
const { browseOUTree, createADUser, checkADUserExists, deleteADUser } = require('../ldapOperations');
|
||||||
const { searchADGroups, addUserToGroups, browseADGroups } = require('../ldapOperations');
|
const { searchADGroups, addUserToGroups, browseADGroups } = require('../ldapOperations');
|
||||||
const { validate, createADUserSchema, deleteADUserSchema } = require('../middleware/validation');
|
const { validate, createADUserSchema, deleteADUserSchema } = require('../middleware/validation');
|
||||||
|
const { toSamAccountName } = require('../umlauts');
|
||||||
|
|
||||||
const router = express.Router();
|
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;
|
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 {
|
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) {
|
if (vorname && nachname) {
|
||||||
sAMAccountName = nachname.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue')
|
sAMAccountName = toSamAccountName(nachname + vorname.charAt(0));
|
||||||
.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 = sAMAccountName.substring(0, 20);
|
||||||
|
|
||||||
const existing = await checkADUserExists(username, sAMAccountName);
|
const existing = await checkADUserExists(username, sAMAccountName);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|||||||
49
backend/umlauts.js
Normal file
49
backend/umlauts.js
Normal file
@@ -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 };
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: gitea.leonkn.de/lxe/workflow-app:DEV2.0-v9
|
image: gitea.leonkn.de/lxe/workflow-app:DEV2.0-v10
|
||||||
ports:
|
ports:
|
||||||
- "3900:5000"
|
- "3900:5000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useToast, ConfirmModal } from './Toast';
|
import { useToast, ConfirmModal } from './Toast';
|
||||||
import { apiFetch } from '../utils/api';
|
import { apiFetch } from '../utils/api';
|
||||||
|
import { toEmailLocalPart, toSamAccountName } from '../utils/umlauts';
|
||||||
|
|
||||||
export default function FillModal({ template, onSubmit, onClose }) {
|
export default function FillModal({ template, onSubmit, onClose }) {
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
@@ -95,7 +96,8 @@ export default function FillModal({ template, onSubmit, onClose }) {
|
|||||||
const parts = sourceFields.map((fieldLabel) => {
|
const parts = sourceFields.map((fieldLabel) => {
|
||||||
const sourceStep = template.steps.find((s) => s.label === fieldLabel);
|
const sourceStep = template.steps.find((s) => s.label === fieldLabel);
|
||||||
if (!sourceStep || !values[sourceStep.id]) return '';
|
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);
|
}).filter(Boolean);
|
||||||
if (parts.length === 0) return '';
|
if (parts.length === 0) return '';
|
||||||
const domain = step.email_domain.startsWith('@') ? step.email_domain : '@' + step.email_domain;
|
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() : '';
|
return sourceStep ? (values[sourceStep.id] || '').trim() : '';
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
if (sourceValues.length > 0) {
|
if (sourceValues.length > 0) {
|
||||||
value = sourceValues.join('.')
|
value = toEmailLocalPart(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, '');
|
|
||||||
} else {
|
} else {
|
||||||
value = '';
|
value = '';
|
||||||
}
|
}
|
||||||
@@ -168,11 +164,7 @@ export default function FillModal({ template, onSubmit, onClose }) {
|
|||||||
const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : '';
|
const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : '';
|
||||||
const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : '';
|
const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : '';
|
||||||
if (vorname && nachname) {
|
if (vorname && nachname) {
|
||||||
value = (nachname + vorname.charAt(0))
|
value = toSamAccountName(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, '');
|
|
||||||
} else {
|
} else {
|
||||||
value = values[s.id] || '';
|
value = values[s.id] || '';
|
||||||
}
|
}
|
||||||
@@ -337,13 +329,7 @@ export default function FillModal({ template, onSubmit, onClose }) {
|
|||||||
return sourceStep ? (values[sourceStep.id] || '').trim() : '';
|
return sourceStep ? (values[sourceStep.id] || '').trim() : '';
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
if (sourceValues.length > 0) {
|
if (sourceValues.length > 0) {
|
||||||
return sourceValues.join('.')
|
return toEmailLocalPart(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 '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -353,11 +339,7 @@ export default function FillModal({ template, onSubmit, onClose }) {
|
|||||||
const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : '';
|
const vorname = vornameStep ? (values[vornameStep.id] || '').trim() : '';
|
||||||
const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : '';
|
const nachname = nachnameStep ? (values[nachnameStep.id] || '').trim() : '';
|
||||||
if (vorname && nachname) {
|
if (vorname && nachname) {
|
||||||
return (nachname + vorname.charAt(0))
|
return toSamAccountName(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 '';
|
return '';
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
49
frontend/src/utils/umlauts.js
Normal file
49
frontend/src/utils/umlauts.js
Normal file
@@ -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, '');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user