feat: мок JWT-аутентификация (RS256, JWKS, cookie)
- src/auth.js: генерация ключей RS256, JWKS endpoint, verify/issue/middleware - views/login.ejs: мок-страница входа с выбором пользователя - server.js: cookie-parser, /login, /logout, auth.middleware - При переключении на прод: JWKS_URL в env → всё остальное без изменений
This commit is contained in:
@@ -1,107 +1,92 @@
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
const { checkConnection } = require('./src/db');
|
||||
const { initAuth } = require('./src/auth');
|
||||
const q = require('./src/queries');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DEV = process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production';
|
||||
|
||||
if (DEV) console.warn('⚠ DEV_MODE активен — аутентификация отключена');
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.urlencoded({ extended: true, limit: '32kb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// ── Health (выше auth — k8s probe не должна требовать токен) ──
|
||||
app.get('/healthz', (req, res) => res.send('OK'));
|
||||
const MOCK_USERS = [
|
||||
{ id: 'admin', label: 'Администратор (WZ01112)', role: 'admin', clientId: 'WZ01112', companyId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', companyName: 'Nubes Admin', email: 'admin@nubes.ru' },
|
||||
{ id: 'test', label: 'Тест (WZ01325)', role: 'user', clientId: 'WZ01325', companyId: '3e64aac6-dcfc-4082-88dc-da19c86555a5', companyName: 'Тест', email: 'tazet@narod.ru' },
|
||||
{ id: 'client2', label: 'Компания 2 (WZ02001)', role: 'user', clientId: 'WZ02001', companyId: '11111111-2222-3333-4444-555555555555', companyName: 'Вторая Компания', email: 'user2@example.com' },
|
||||
];
|
||||
|
||||
// ── Auth middleware ──
|
||||
app.use((req, res, next) => {
|
||||
if (DEV) {
|
||||
req.user = { email: 'dev@test.local', clientId: 'WZ01325', companyId: null, companyName: 'DEV' };
|
||||
return next();
|
||||
}
|
||||
const auth = req.headers.authorization || '';
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
|
||||
req.user = {
|
||||
email: payload.email || 'unknown',
|
||||
clientId: payload.ClientID,
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name || payload.ClientID,
|
||||
};
|
||||
} catch { req.user = {}; }
|
||||
// Без clientId — 401, анонимы не должны делить NULL-компанию
|
||||
if (!req.user.clientId) return res.status(401).send('Unauthorized');
|
||||
next();
|
||||
});
|
||||
async function start() {
|
||||
const auth = await initAuth();
|
||||
|
||||
// ── Главная ──
|
||||
app.get('/', async (req, res) => {
|
||||
const { clientId, companyName } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', {
|
||||
entries, limit, used: entries.length, user: req.user,
|
||||
error: req.query.error || null,
|
||||
message: req.query.message || null,
|
||||
wasNormalized: req.query.wasNormalized === '1',
|
||||
});
|
||||
} catch (e) {
|
||||
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: 'Ошибка загрузки', message: null, wasNormalized: false });
|
||||
}
|
||||
});
|
||||
app.get('/healthz', (req, res) => res.send('OK'));
|
||||
|
||||
// ── Создать ──
|
||||
app.post('/add', async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const result = await q.createEntry(company.id, value, comment, email);
|
||||
const message = result.wasNormalized
|
||||
? `Адрес нормализован в ${result.entry.value_cidr}`
|
||||
: 'Добавлено';
|
||||
const wasNormalized = result.wasNormalized ? '1' : '0';
|
||||
res.redirect(`/?message=${encodeURIComponent(message)}&wasNormalized=${wasNormalized}`);
|
||||
} catch (e) {
|
||||
res.redirect('/?error=' + encodeURIComponent(e.message));
|
||||
}
|
||||
});
|
||||
if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler);
|
||||
|
||||
// ── Удалить (soft) ──
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
await q.deleteEntry(req.params.id, company.id, email);
|
||||
res.redirect('/');
|
||||
} catch (e) {
|
||||
res.redirect('/?error=' + encodeURIComponent(e.message));
|
||||
}
|
||||
});
|
||||
app.get('/login', (req, res) => res.render('login', { users: MOCK_USERS, error: req.query.error || null }));
|
||||
|
||||
// ── Экспорт (только для своей компании) ──
|
||||
app.get('/export', async (req, res) => {
|
||||
try {
|
||||
app.post('/login', async (req, res) => {
|
||||
const user = MOCK_USERS.find(u => u.id === req.body.user);
|
||||
if (!user) return res.redirect('/login?error=' + encodeURIComponent('Пользователь не найден'));
|
||||
try {
|
||||
const token = await auth.issue({ clientId: user.clientId, companyId: user.companyId, companyName: user.companyName, email: user.email });
|
||||
res.cookie('jwt', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 });
|
||||
res.redirect(req.query.returnTo || '/');
|
||||
} catch (e) { res.redirect('/login?error=' + encodeURIComponent('Ошибка выпуска токена')); }
|
||||
});
|
||||
|
||||
app.get('/logout', (req, res) => { res.clearCookie('jwt'); res.redirect('/login'); });
|
||||
|
||||
app.use(auth.middleware);
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
const { clientId, companyName } = req.user;
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const cidrs = await q.getExportCIDRs(company.id);
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(cidrs.join('\n') + '\n');
|
||||
} catch (e) {
|
||||
res.status(500).send('Export error');
|
||||
}
|
||||
});
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', { entries, limit, used: entries.length, user: req.user, error: req.query.error || null, message: req.query.message || null, wasNormalized: req.query.wasNormalized === '1' });
|
||||
} catch (e) { res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: 'Ошибка загрузки', message: null, wasNormalized: false }); }
|
||||
});
|
||||
|
||||
// ── Старт ──
|
||||
checkConnection()
|
||||
.then(() => console.log('DB connected'))
|
||||
.catch(e => console.error('DB not ready:', e.message));
|
||||
app.post('/add', async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const result = await q.createEntry(company.id, value, comment, email);
|
||||
const message = result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено';
|
||||
const wasNormalized = result.wasNormalized ? '1' : '0';
|
||||
res.redirect('/?message=' + encodeURIComponent(message) + '&wasNormalized=' + wasNormalized);
|
||||
} catch (e) { res.redirect('/?error=' + encodeURIComponent(e.message)); }
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
await q.deleteEntry(req.params.id, company.id, email);
|
||||
res.redirect('/');
|
||||
} catch (e) { res.redirect('/?error=' + encodeURIComponent(e.message)); }
|
||||
});
|
||||
|
||||
app.get('/export', async (req, res) => {
|
||||
try {
|
||||
const { clientId, companyName } = req.user;
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const cidrs = await q.getExportCIDRs(company.id);
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(cidrs.join('\n') + '\n');
|
||||
} catch (e) { res.status(500).send('Export error'); }
|
||||
});
|
||||
|
||||
checkConnection().then(() => console.log('DB connected')).catch(e => console.error('DB not ready:', e.message));
|
||||
app.listen(PORT, () => console.log('Server on port ' + PORT));
|
||||
}
|
||||
|
||||
start().catch(e => { console.error('Startup error:', e); process.exit(1); });
|
||||
|
||||
Reference in New Issue
Block a user