231 lines
7.5 KiB
JavaScript
231 lines
7.5 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const express = require('express');
|
|
const config = require('./lib/config');
|
|
const ldapClient = require('./lib/ldapClient');
|
|
const adminApi = require('./lib/adminApi');
|
|
const registerApi = require('./lib/registerApi');
|
|
const directoryApi = require('./lib/directoryApi');
|
|
const gosuslugiApi = require('./lib/gosuslugiApi');
|
|
const zupApi = require('./lib/zupApi');
|
|
const favicon = require('./lib/favicon');
|
|
const adminSettings = require('./lib/adminSettings');
|
|
const sessions = require('./lib/sessions');
|
|
const audit = require('./lib/auditLog');
|
|
const rateLimit = require('./lib/rateLimit');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
app.use(express.static(path.join(__dirname, 'public'), {
|
|
setHeaders(res, filePath) {
|
|
if (/\.(css|js|woff2?|png|jpe?g|svg|ico)$/i.test(filePath)) {
|
|
res.setHeader('Cache-Control', 'public, max-age=604800');
|
|
} else {
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
}
|
|
}
|
|
}));
|
|
|
|
app.use((req, res, next) => {
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
next();
|
|
});
|
|
|
|
function getToken(req) {
|
|
const header = req.headers.authorization || '';
|
|
if (header.startsWith('Bearer ')) return header.slice(7);
|
|
return null;
|
|
}
|
|
|
|
function timingSafeEqualStr(a, b) {
|
|
const ba = Buffer.from(String(a));
|
|
const bb = Buffer.from(String(b));
|
|
if (ba.length !== bb.length) return false;
|
|
return crypto.timingSafeEqual(ba, bb);
|
|
}
|
|
|
|
function adminAliasesSet() {
|
|
const aliases = new Set(ldapClient.buildBindNames(config.adminLogin).map((n) => n.toLowerCase()));
|
|
aliases.add(config.adminLogin.split('@')[0].split('\\').pop().toLowerCase());
|
|
return aliases;
|
|
}
|
|
|
|
function basicAuthOk(req) {
|
|
const header = req.headers.authorization || '';
|
|
if (!header.startsWith('Basic ')) return false;
|
|
if (!config.adminLogin || !config.adminPassword) return false;
|
|
let decoded;
|
|
try {
|
|
decoded = Buffer.from(header.slice(6), 'base64').toString('utf8');
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
const idx = decoded.indexOf(':');
|
|
if (idx === -1) return false;
|
|
const username = decoded.slice(0, idx);
|
|
const password = decoded.slice(idx + 1);
|
|
if (!adminAliasesSet().has(String(username).toLowerCase())) return false;
|
|
return timingSafeEqualStr(password, config.adminPassword);
|
|
}
|
|
|
|
function authEnabled() {
|
|
return adminSettings.getSettings().authEnabled !== false;
|
|
}
|
|
|
|
app.get('/', (req, res) => res.redirect(302, authEnabled() ? '/auth' : '/register'));
|
|
|
|
app.get('/auth', (req, res) => {
|
|
if (!authEnabled()) return res.redirect(302, '/register');
|
|
res.sendFile(path.join(__dirname, 'public', 'auth.html'));
|
|
});
|
|
|
|
app.get('/admin', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
|
|
});
|
|
|
|
app.get('/gosuslugi', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'gosuslugi.html'));
|
|
});
|
|
|
|
app.get('/zup', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'zup.html'));
|
|
});
|
|
|
|
app.get('/register', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'register.html'));
|
|
});
|
|
|
|
app.get('/change-password', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'change-password.html'));
|
|
});
|
|
|
|
app.get('/help', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'help.html'));
|
|
});
|
|
|
|
app.get('/favicon.ico', (req, res) => {
|
|
const ico = favicon.getFavicon();
|
|
if (!ico) return res.status(404).end();
|
|
res.setHeader('Cache-Control', 'public, max-age=604800');
|
|
res.type('image/x-icon');
|
|
res.send(ico);
|
|
});
|
|
|
|
app.get('/health', async (req, res) => {
|
|
try {
|
|
await ldapClient.testConnection();
|
|
res.json({ status: 'ok', ldap: 'connected' });
|
|
} catch (err) {
|
|
res.status(503).json({ status: 'error', error: err.message });
|
|
}
|
|
});
|
|
|
|
app.get('/status', (req, res) => {
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
if (!sessions.getSession(getToken(req)) && !basicAuthOk(req)) {
|
|
res.setHeader('WWW-Authenticate', 'Basic realm="LDAP Auth Admin"');
|
|
return res.status(401).json({ success: false, error: 'Требуется авторизация' });
|
|
}
|
|
res.json({
|
|
status: 'running',
|
|
service: 'LDAP Auth',
|
|
ldap_url: config.ldapUrl,
|
|
base_dn: config.baseDN,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
app.post('/api/auth', async (req, res) => {
|
|
const { username, password } = req.body || {};
|
|
const ip = rateLimit.getClientIp(req);
|
|
if (rateLimit.isBlocked(ip)) {
|
|
return res.status(429).json({ success: false, error: 'Слишком много попыток, попробуйте через час' });
|
|
}
|
|
const all = req.query.all === '1';
|
|
const description = req.query.description === '1';
|
|
const groups = req.query.groups === '1';
|
|
|
|
const modeCount = [all, description, groups].filter(Boolean).length;
|
|
if (modeCount > 1) {
|
|
return res.status(400).json({ success: false, error: 'Conflicting parameters: use only one of description=1, groups=1, or all=1' });
|
|
}
|
|
if (!username) {
|
|
return res.status(400).json({ success: false, error: 'Username is required' });
|
|
}
|
|
|
|
let mode = 'basic';
|
|
if (description) mode = 'description';
|
|
else if (groups) mode = 'groups';
|
|
else if (all) mode = 'all';
|
|
|
|
try {
|
|
const result = await ldapClient.authenticate(username, password, mode);
|
|
rateLimit.reset(ip);
|
|
audit.logAuthEntry({ ip, username, success: true });
|
|
res.json(result);
|
|
} catch (err) {
|
|
if (err.status === 401) {
|
|
rateLimit.recordFailure(ip);
|
|
audit.logAuthEntry({ ip, username, success: false });
|
|
}
|
|
res.status(err.status || 401).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
app.use('/api/admin', adminApi);
|
|
app.use('/api/register', registerApi);
|
|
app.use('/api/directory', directoryApi);
|
|
app.use('/api/gosuslugi', gosuslugiApi);
|
|
app.use('/api/zup', zupApi);
|
|
|
|
app.get('/vendor/xlsx.full.min.js', (req, res) => {
|
|
res.setHeader('Cache-Control', 'public, max-age=604800');
|
|
res.sendFile(path.join(__dirname, 'node_modules', 'xlsx', 'dist', 'xlsx.full.min.js'));
|
|
});
|
|
|
|
app.use((err, req, res, next) => {
|
|
const status = err.status || err.statusCode || 500;
|
|
if (status >= 500) console.error(err);
|
|
res.status(status).json({ success: false, error: status >= 500 ? 'Внутренняя ошибка сервера' : (err.message || 'Некорректный запрос') });
|
|
});
|
|
|
|
async function main() {
|
|
console.log('=== LDAP Auth Service Starting ===');
|
|
console.log(`LDAP URL: ${config.ldapUrl}`);
|
|
console.log(`Base DN: ${config.baseDN}`);
|
|
console.log(`Admin: ${config.adminLogin || '(not set)'}`);
|
|
console.log(`Port: ${config.port}`);
|
|
console.log('==================================');
|
|
|
|
await favicon.loadFavicon();
|
|
if (favicon.getFavicon()) {
|
|
console.log('✓ Favicon prepared from logo.png');
|
|
} else {
|
|
console.warn('⚠ logo.png not found or invalid — favicon disabled');
|
|
}
|
|
|
|
if (!config.adminLogin || !config.adminPassword) {
|
|
console.warn('⚠ ADMIN_LOGIN / ADMIN_PASSWORD not set in .env - admin panel disabled');
|
|
} else {
|
|
try {
|
|
await ldapClient.testConnection();
|
|
console.log('✓ LDAP connection test passed');
|
|
} catch (err) {
|
|
console.warn(`⚠ LDAP connection test failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
app.listen(config.port, () => {
|
|
console.log(`🚀 Server running on http://localhost:${config.port}`);
|
|
console.log(`🌐 Web auth: http://localhost:${config.port}/auth`);
|
|
console.log(`🛠 Admin panel: http://localhost:${config.port}/admin`);
|
|
console.log(`📊 Health: http://localhost:${config.port}/health`);
|
|
});
|
|
}
|
|
|
|
main(); |