zup2
This commit is contained in:
@@ -30,6 +30,7 @@ function mapLdapError(err) {
|
||||
case 50: return new LdapError('Недостаточно прав для выполнения операции', code, 403);
|
||||
case 53: return new LdapError('Операция запрещена сервером', code, 500);
|
||||
case 19: return new LdapError('Нарушение ограничений (например, слишком короткий пароль)', code, 400);
|
||||
case 21: return new LdapError('Недопустимое значение атрибута (проверьте ФИО, телефон, почту)', code, 400);
|
||||
case 14: return new LdapError('Операция недоступна', code, 500);
|
||||
case 52: return new LdapError('Сервер недоступен', code, 500);
|
||||
case 80: return new LdapError('Не удалось подключиться к LDAP-серверу', code, 502);
|
||||
@@ -383,6 +384,7 @@ function parseUser(entry) {
|
||||
full_name: entry.displayName || entry.cn || '',
|
||||
given_name: entry.givenName || '',
|
||||
sn: entry.sn || '',
|
||||
middle_name: entry.middleName || '',
|
||||
email: entry.mail || '',
|
||||
phone: entry.telephoneNumber || '',
|
||||
position: entry.description || '',
|
||||
@@ -400,7 +402,7 @@ async function listUsers() {
|
||||
const entries = await searchEntries(client, config.baseDN, {
|
||||
scope: 'sub',
|
||||
filter: '(&(objectClass=user)(objectCategory=person))',
|
||||
attributes: ['dn', 'sAMAccountName', 'displayName', 'cn', 'givenName', 'sn',
|
||||
attributes: ['dn', 'sAMAccountName', 'displayName', 'cn', 'givenName', 'sn', 'middleName',
|
||||
'mail', 'telephoneNumber', 'description', 'userPrincipalName', 'userAccountControl', 'memberOf']
|
||||
});
|
||||
return entries.map((e) => parseUser(entryToObject(e)));
|
||||
@@ -557,7 +559,24 @@ async function createUser({ ouDn, fio, login, password, email, phone, position,
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(login, { fio, email, phone, position, logonScript, homeDrive, homeBasePath, password, groupCNs }) {
|
||||
function cleanValue(value, maxLen) {
|
||||
return String(value || '')
|
||||
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, maxLen || 200);
|
||||
}
|
||||
|
||||
function splitFioParts(fio) {
|
||||
const parts = cleanValue(fio).split(' ');
|
||||
return {
|
||||
lastName: parts[0] || '',
|
||||
firstName: parts[1] || '',
|
||||
middleName: parts.slice(2).join(' ') || ''
|
||||
};
|
||||
}
|
||||
|
||||
async function updateUser(login, { fio, lastName, firstName, middleName, email, phone, position, logonScript, homeDrive, homeBasePath, password, groupCNs }) {
|
||||
const client = await connect();
|
||||
try {
|
||||
await bind(client, config.adminLogin, config.adminPassword);
|
||||
@@ -566,37 +585,50 @@ async function updateUser(login, { fio, email, phone, position, logonScript, hom
|
||||
const userDN = entry.dn;
|
||||
const changes = [];
|
||||
|
||||
if (fio !== undefined) {
|
||||
changes.push({ operation: 'replace', modification: { displayName: [fio] } });
|
||||
const nameParts = (fio || '').trim().split(/\s+/);
|
||||
changes.push({
|
||||
operation: 'replace',
|
||||
modification: { givenName: [nameParts[0] || ''] }
|
||||
});
|
||||
changes.push({
|
||||
operation: 'replace',
|
||||
modification: { sn: [nameParts.slice(1).join(' ') || ''] }
|
||||
});
|
||||
const hasNameFields = lastName !== undefined || firstName !== undefined || middleName !== undefined;
|
||||
if (fio !== undefined || hasNameFields) {
|
||||
let lname = lastName !== undefined ? cleanValue(lastName, 64) : null;
|
||||
let fname = firstName !== undefined ? cleanValue(firstName, 64) : null;
|
||||
let mname = middleName !== undefined ? cleanValue(middleName, 64) : null;
|
||||
if (fio !== undefined && !hasNameFields) {
|
||||
const parts = splitFioParts(fio);
|
||||
if (lname === null) lname = parts.lastName;
|
||||
if (fname === null) fname = parts.firstName;
|
||||
if (mname === null) mname = parts.middleName;
|
||||
}
|
||||
const display = [lname, fname, mname].filter(Boolean).join(' ');
|
||||
if (display) {
|
||||
changes.push({ operation: 'replace', modification: { displayName: [display] } });
|
||||
}
|
||||
if (lname !== null && lname) {
|
||||
changes.push({ operation: 'replace', modification: { sn: [lname] } });
|
||||
}
|
||||
if (fname !== null && fname) {
|
||||
changes.push({ operation: 'replace', modification: { givenName: [fname] } });
|
||||
}
|
||||
if (mname !== null && mname) {
|
||||
changes.push({ operation: 'replace', modification: { middleName: [mname] } });
|
||||
}
|
||||
}
|
||||
if (email !== undefined) {
|
||||
const emailVal = String(email || '').trim();
|
||||
const emailVal = cleanValue(email, 254);
|
||||
if (emailVal) {
|
||||
changes.push({ operation: 'replace', modification: { mail: [emailVal] } });
|
||||
}
|
||||
}
|
||||
if (phone !== undefined) {
|
||||
changes.push({ operation: 'replace', modification: { telephoneNumber: [phone || ''] } });
|
||||
changes.push({ operation: 'replace', modification: { telephoneNumber: [cleanValue(phone, 60)] } });
|
||||
}
|
||||
if (position !== undefined) {
|
||||
const positionVal = String(position).trim() || '';
|
||||
const positionVal = cleanValue(position, 120);
|
||||
changes.push({ operation: 'replace', modification: { description: [positionVal] } });
|
||||
changes.push({ operation: 'replace', modification: { title: [positionVal] } });
|
||||
}
|
||||
if (logonScript !== undefined) {
|
||||
changes.push({ operation: 'replace', modification: { scriptPath: [String(logonScript).trim() || ''] } });
|
||||
changes.push({ operation: 'replace', modification: { scriptPath: [cleanValue(logonScript, 255)] } });
|
||||
}
|
||||
if (homeDrive !== undefined) {
|
||||
changes.push({ operation: 'replace', modification: { homeDrive: [String(homeDrive).trim() || ''] } });
|
||||
changes.push({ operation: 'replace', modification: { homeDrive: [cleanValue(homeDrive, 3)] } });
|
||||
}
|
||||
if (homeBasePath !== undefined) {
|
||||
changes.push({
|
||||
|
||||
@@ -98,6 +98,9 @@ router.get('/users', requireZupAuth, async (req, res) => {
|
||||
users: users.map((u) => ({
|
||||
login: u.login,
|
||||
fio: u.full_name || '',
|
||||
first_name: u.given_name || '',
|
||||
last_name: u.sn || '',
|
||||
middle_name: u.middle_name || '',
|
||||
phone: u.phone || '',
|
||||
email: u.email || '',
|
||||
position: u.position || '',
|
||||
@@ -113,7 +116,7 @@ router.get('/users', requireZupAuth, async (req, res) => {
|
||||
router.put('/users/:login', requireZupAuth, async (req, res) => {
|
||||
const login = String(req.params.login || '').trim();
|
||||
const actor = (req.session.user && req.session.user.username) || '';
|
||||
const { fio, position, phone, email } = req.body || {};
|
||||
const { fio, lastName, firstName, middleName, position, phone, email } = req.body || {};
|
||||
try {
|
||||
if (!login) return res.status(400).json({ success: false, error: 'Логин не указан' });
|
||||
if (String(login).toLowerCase() === String(actor).toLowerCase()) {
|
||||
@@ -122,8 +125,24 @@ router.put('/users/:login', requireZupAuth, async (req, res) => {
|
||||
if (zupAccess.isAdminLogin(login)) {
|
||||
return res.status(400).json({ success: false, error: 'Нельзя редактировать учётную запись администратора' });
|
||||
}
|
||||
const newFio = String(fio || '').trim();
|
||||
if (!newFio) return res.status(400).json({ success: false, error: 'ФИО обязательно' });
|
||||
const hasNameFields = lastName !== undefined || firstName !== undefined || middleName !== undefined;
|
||||
let newLastName = String(lastName !== undefined ? lastName : '').trim().slice(0, 64);
|
||||
let newFirstName = String(firstName !== undefined ? firstName : '').trim().slice(0, 64);
|
||||
let newMiddleName = String(middleName !== undefined ? middleName : '').trim().slice(0, 64);
|
||||
if (fio !== undefined && !hasNameFields) {
|
||||
// обратная совместимость: строка «Фамилия Имя Отчество»
|
||||
const parts = String(fio).trim().split(/\s+/);
|
||||
newLastName = parts[0] || '';
|
||||
newFirstName = parts[1] || '';
|
||||
newMiddleName = parts.slice(2).join(' ') || '';
|
||||
}
|
||||
if (hasNameFields) {
|
||||
if (!newLastName || !newFirstName) {
|
||||
return res.status(400).json({ success: false, error: 'Укажите фамилию и имя' });
|
||||
}
|
||||
} else if (!newLastName) {
|
||||
return res.status(400).json({ success: false, error: 'ФИО обязательно' });
|
||||
}
|
||||
const newEmail = String(email || '').trim();
|
||||
if (newEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
|
||||
return res.status(400).json({ success: false, error: 'Некорректный email' });
|
||||
@@ -133,7 +152,9 @@ router.put('/users/:login', requireZupAuth, async (req, res) => {
|
||||
|
||||
await ldapClient.getUser(login);
|
||||
const result = await ldapClient.updateUser(login, {
|
||||
fio: newFio,
|
||||
lastName: newLastName,
|
||||
firstName: newFirstName,
|
||||
middleName: newMiddleName,
|
||||
position: newPosition,
|
||||
email: newEmail,
|
||||
phone: newPhone
|
||||
|
||||
@@ -171,9 +171,19 @@
|
||||
<div class="modal">
|
||||
<h3 id="editUserTitle">Редактирование сотрудника</h3>
|
||||
<input type="hidden" id="eu_login">
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<label>Фамилия *</label>
|
||||
<input type="text" id="eu_lastName" maxlength="64">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Имя *</label>
|
||||
<input type="text" id="eu_firstName" maxlength="64">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ФИО *</label>
|
||||
<input type="text" id="eu_fio">
|
||||
<label>Отчество</label>
|
||||
<input type="text" id="eu_middleName" maxlength="64">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Должность</label>
|
||||
@@ -364,6 +374,7 @@
|
||||
if (!s && !emptyOk) continue;
|
||||
if (!distinct.includes(s)) distinct.push(s);
|
||||
}
|
||||
distinct.sort((a, b) => String(a).localeCompare(String(b), 'ru', { sensitivity: 'base' }));
|
||||
sel.innerHTML = '<option value="">' + allLabel + '</option>' +
|
||||
distinct.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
|
||||
if (prev && distinct.includes(prev)) sel.value = prev;
|
||||
@@ -400,7 +411,7 @@
|
||||
(!fPos || u.position === fPos) &&
|
||||
(!fStatus || (fStatus === 'disabled' ? u.disabled : !u.disabled)) &&
|
||||
(!fBad || fioHasIssues(u.fio))
|
||||
);
|
||||
).sort((a, b) => String(a.fio || a.login).localeCompare(String(b.fio || b.login), 'ru', { sensitivity: 'base' }));
|
||||
|
||||
const badCount = emps.filter(u => fioHasIssues(u.fio)).length;
|
||||
$('empCount').textContent = 'Показано: ' + empFiltered.length + ' из ' + emps.length + (badCount ? ' · некорректных ФИО: ' + badCount : '');
|
||||
@@ -464,7 +475,9 @@
|
||||
const u = usersCache.find(x => x.login === login);
|
||||
if (!u) return;
|
||||
editUserLogin = login;
|
||||
$('eu_fio').value = u.fio || '';
|
||||
$('eu_lastName').value = u.last_name || '';
|
||||
$('eu_firstName').value = u.first_name || '';
|
||||
$('eu_middleName').value = u.middle_name || '';
|
||||
$('eu_position').value = u.position || '';
|
||||
$('eu_phone').value = u.phone || '';
|
||||
$('eu_email').value = u.email || '';
|
||||
@@ -475,12 +488,14 @@
|
||||
$('euSaveBtn').addEventListener('click', async () => {
|
||||
if (!editUserLogin) return;
|
||||
const body = {
|
||||
fio: $('eu_fio').value.trim(),
|
||||
lastName: $('eu_lastName').value.trim(),
|
||||
firstName: $('eu_firstName').value.trim(),
|
||||
middleName: $('eu_middleName').value.trim(),
|
||||
position: $('eu_position').value.trim(),
|
||||
phone: $('eu_phone').value.trim(),
|
||||
email: $('eu_email').value.trim()
|
||||
};
|
||||
if (!body.fio) { showMsg($('appMsg'), 'ФИО обязательно', 'error'); return; }
|
||||
if (!body.lastName || !body.firstName) { showMsg($('appMsg'), 'Укажите фамилию и имя', 'error'); return; }
|
||||
try {
|
||||
await api('/api/zup/users/' + encodeURIComponent(editUserLogin), { method: 'PUT', body: JSON.stringify(body) });
|
||||
$('editUserOverlay').classList.add('hidden');
|
||||
|
||||
Reference in New Issue
Block a user