This commit is contained in:
2026-08-26 14:16:14 +05:00
parent 3059741b1a
commit efdefe1c4c
4 changed files with 60 additions and 10 deletions

View File

@@ -108,6 +108,8 @@ router.put('/settings', requireAuth, (req, res) => {
if (typeof body.logonScript === 'string') patch.logonScript = body.logonScript;
if (typeof body.homeDrive === 'string') patch.homeDrive = body.homeDrive;
if (typeof body.homeBasePath === 'string') patch.homeBasePath = body.homeBasePath;
if (typeof body.crmUrl === 'string') patch.crmUrl = body.crmUrl;
if (typeof body.quickLinksUrl === 'string') patch.quickLinksUrl = body.quickLinksUrl;
if (Object.keys(patch).length === 0) {
return res.status(400).json({ success: false, error: 'Нет данных для сохранения' });
}
@@ -273,7 +275,11 @@ router.post('/requests/:id/approve', requireAuth, async (req, res) => {
groupCNs
});
try {
await mailer.sendApproved(record.email, { login: record.login, password: record.password, fio: record.fio, position: record.position });
await mailer.sendApproved(record.email, {
login: record.login, password: record.password, fio: record.fio, position: record.position,
crmUrl: settings.crmUrl, quickLinksUrl: settings.quickLinksUrl,
netbios: config.domain.split('.')[0].toLowerCase()
});
requests.markProcessed(record.id, { phase: 'approved', emailSent: true });
} catch (mailErr) {
requests.markProcessed(record.id, { phase: 'approved', emailSent: false });
@@ -322,7 +328,12 @@ router.post('/requests/:id/resend', requireAuth, async (req, res) => {
await mailer.sendPasswordChangeDenied(record.email, { fio: record.fio, reason: record.reason });
}
} else if (record.phase === 'approved') {
await mailer.sendApproved(record.email, { login: record.login, password: record.password, fio: record.fio, position: record.position });
const s = adminSettings.getSettings();
await mailer.sendApproved(record.email, {
login: record.login, password: record.password, fio: record.fio, position: record.position,
crmUrl: s.crmUrl, quickLinksUrl: s.quickLinksUrl,
netbios: config.domain.split('.')[0].toLowerCase()
});
} else {
await mailer.sendDenied(record.email, { fio: record.fio, reason: record.reason });
}

View File

@@ -19,12 +19,15 @@ function getSettings() {
apiKey: typeof parsed.apiKey === 'string' ? parsed.apiKey : '',
logonScript: typeof parsed.logonScript === 'string' ? parsed.logonScript : 'PST.bat',
homeDrive: typeof parsed.homeDrive === 'string' ? parsed.homeDrive : 'Z:',
homeBasePath: typeof parsed.homeBasePath === 'string' ? parsed.homeBasePath : '\\\\dc\\Teachers'
homeBasePath: typeof parsed.homeBasePath === 'string' ? parsed.homeBasePath : '\\\\dc\\Teachers',
crmUrl: typeof parsed.crmUrl === 'string' ? parsed.crmUrl : '',
quickLinksUrl: typeof parsed.quickLinksUrl === 'string' ? parsed.quickLinksUrl : ''
};
} catch (err) {
return {
ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '',
logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers'
logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers',
crmUrl: '', quickLinksUrl: ''
};
}
}
@@ -42,6 +45,8 @@ function updateSettings(patch) {
if (patch && typeof patch.logonScript === 'string') next.logonScript = patch.logonScript;
if (patch && typeof patch.homeDrive === 'string') next.homeDrive = patch.homeDrive;
if (patch && typeof patch.homeBasePath === 'string') next.homeBasePath = patch.homeBasePath;
if (patch && typeof patch.crmUrl === 'string') next.crmUrl = patch.crmUrl;
if (patch && typeof patch.quickLinksUrl === 'string') next.quickLinksUrl = patch.quickLinksUrl;
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2), 'utf8');
return next;

View File

@@ -38,12 +38,15 @@ function sendVerificationCode(email, code) {
});
}
function sendApproved(email, { login, password, fio, position }) {
function sendApproved(email, { login, password, fio, position, crmUrl, quickLinksUrl, netbios }) {
const positionLine = position ? `\nДолжность: ${position}` : '';
const netbiosName = (netbios || config.domain.split('.')[0]).toLowerCase();
const crmLine = crmUrl ? `\n\nАдрес СРМ (задачник): ${crmUrl}` : '';
const linksLine = quickLinksUrl ? `\nСервис быстрых ссылок: ${quickLinksUrl}` : '';
return sendMail({
to: email,
subject: 'Регистрация одобрена',
text: `Здравствуйте, ${fio}!\n\nВаша заявка на регистрацию одобрена.\n\nВаши данные для входа:\nЛогин: ${login}\nПароль: ${password}${positionLine}`
text: `Здравствуйте, ${fio}!\n\nВаша заявка на регистрацию одобрена.\n\nВаши данные для входа:\nЛогин: ${login}\nПароль: ${password}${positionLine}\n\nИнструкция по входу в систему:\n• Чтобы зайти под своей учётной записью, на экране входа выберите «Другой пользователь».\n• Логин и пароль едины и для входа на ПК, и для задачника.\n• Если простой логин и пароль не принимаются, введите учётную запись полностью с указанием домена: ${netbiosName}\\${login}\n (пример: ${netbiosName}\\${login})${crmLine}${linksLine}`
});
}

View File

@@ -95,6 +95,20 @@
</div>
<input type="text" id="cfgHomeBasePath" class="api-key-input" placeholder="\\dc\Teachers">
</div>
<div class="settings-row">
<div class="settings-label">
Адрес СРМ (задачник)
<div class="hint">Отображается в письме об одобрении регистрации</div>
</div>
<input type="text" id="cfgCrmUrl" class="api-key-input" placeholder="https://...">
</div>
<div class="settings-row">
<div class="settings-label">
Сервис быстрых ссылок
<div class="hint">Отображается в письме об одобрении регистрации</div>
</div>
<input type="text" id="cfgQuickLinksUrl" class="api-key-input" placeholder="https://...">
</div>
</div>
<div class="toolbar">
@@ -281,7 +295,7 @@
<script>
const TOKEN_KEY = 'ldap_admin_token';
const USER_KEY = 'ldap_admin_user';
let adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers' };
let adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers', crmUrl: '', quickLinksUrl: '' };
let currentMemberGroups = [];
let requestsCache = { pending: [], history: [] };
let reqTab = 'pending';
@@ -387,15 +401,17 @@
async function loadSettings() {
try {
const data = await api('/api/admin/settings');
adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers', ...(data.settings || {}) };
adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers', crmUrl: '', quickLinksUrl: '', ...(data.settings || {}) };
$('reqPhoneRequired').checked = !!adminSettings.phoneRequired;
$('reqAuthEnabled').checked = adminSettings.authEnabled !== false;
$('apiKeyField').value = adminSettings.apiKey || '';
$('cfgLogonScript').value = adminSettings.logonScript || '';
$('cfgHomeDrive').value = adminSettings.homeDrive || '';
$('cfgHomeBasePath').value = adminSettings.homeBasePath || '';
$('cfgCrmUrl').value = adminSettings.crmUrl || '';
$('cfgQuickLinksUrl').value = adminSettings.quickLinksUrl || '';
} catch (err) {
adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers' };
adminSettings = { ouDn: '', defaultGroups: [], phoneRequired: false, authEnabled: true, apiKey: '', logonScript: 'PST.bat', homeDrive: 'Z:', homeBasePath: '\\\\dc\\Teachers', crmUrl: '', quickLinksUrl: '' };
}
}
@@ -552,11 +568,13 @@
const date = new Date(r.createdAt || Date.now()).toLocaleString('ru-RU');
const resend = (r.phase === 'approved' || r.phase === 'denied') && !r.emailSent
? `<button class="btn btn-secondary" onclick="resendEmail('${r.id}')">Письмо не отправлено — повторить</button>` : '';
const resendApproved = reqTab === 'history' && r.type !== 'password-change' && r.phase === 'approved'
? `<button class="btn btn-secondary" onclick="resendApprovedEmail('${r.id}')">Отправить письмо повторно</button>` : '';
return `<div class="req-card">
<div class="req-head">${badge}${typeBadge}<span class="req-fio">${escapeHtml(r.fio || '')}</span><span class="req-date">${date}</span></div>
<div class="req-meta">Логин: ${escapeHtml(r.login || '—')} · Почта: ${escapeHtml(r.email || '—')}${r.phone ? ' · Телефон: ' + escapeHtml(r.phone) : ''}${r.position ? ' · Должность: ' + escapeHtml(r.position) : ''}</div>
${groups}${reason}
<div class="actions" style="margin-top:8px">${reqTab === 'pending' ? `<button class="btn btn-secondary" onclick="openRequestModal('${r.id}')">Рассмотреть</button>` : ''}${resend}</div>
<div class="actions" style="margin-top:8px">${reqTab === 'pending' ? `<button class="btn btn-secondary" onclick="openRequestModal('${r.id}')">Рассмотреть</button>` : ''}${resend}${resendApproved}</div>
</div>`;
}).join('');
}
@@ -666,6 +684,17 @@
}
}
async function resendApprovedEmail(id) {
if (!confirm('Отправить письмо с данными об одобрении ещё раз?')) return;
try {
await api(`/api/admin/requests/${id}/resend`, { method: 'POST' });
showMsg($('adminMsg'), 'Письмо отправлено повторно', 'success');
await loadRequests();
} catch (err) {
showMsg($('adminMsg'), err.message, 'error');
}
}
// ================= Security journal / whitelist =================
let journalType = 'admin';
@@ -869,6 +898,8 @@
bindSettingField('cfgLogonScript', 'logonScript');
bindSettingField('cfgHomeDrive', 'homeDrive');
bindSettingField('cfgHomeBasePath', 'homeBasePath');
bindSettingField('cfgCrmUrl', 'crmUrl');
bindSettingField('cfgQuickLinksUrl', 'quickLinksUrl');
$('apiKeyCopyBtn').addEventListener('click', async () => {
const val = $('apiKeyField').value;