refactor: шаг 5 — /login /logout вынесены в src/routes/auth.js

This commit is contained in:
2026-05-30 13:39:04 +03:00
parent f91a882379
commit 55d26c1ece
2 changed files with 101 additions and 33 deletions
+93
View File
@@ -0,0 +1,93 @@
/**
* src/routes/auth.js — маршруты аутентификации (mock-режим).
*
* Маршруты:
* GET /login — страница выбора пользователя (мок-форма)
* POST /login — обработка выбора: выпускает JWT, кладёт в cookie
* GET /logout — очищает cookie, возвращает на /login
*
* Зависимости (приходят через deps):
* auth — объект из initAuth(): { issue, middleware, jwksHandler }
* doubleCsrfProtection — middleware проверки CSRF-токена
* generateCsrfToken — функция генерации токена для передачи в шаблон
* MOCK_USERS — список мок-пользователей (из src/config.js)
*
* В продакшене этот файл заменяется на редирект к Keycloak (OIDC Authorization Code Flow).
* Маршруты регистрируются ДО auth.middleware — т.е. доступны без авторизации.
*
* @param {{ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }} deps
* @returns {import('express').Router}
*/
'use strict';
const { Router } = require('express');
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) {
const router = Router();
// GET /login — отдаём форму с выпадающим списком мок-пользователей.
// Генерируем CSRF-токен здесь, чтобы он попал в скрытое поле формы.
// Без этого POST /login будет отклонён doubleCsrfProtection.
router.get('/login', (req, res) => {
// generateCsrfToken одновременно: устанавливает CSRF cookie и возвращает токен-строку
const csrfToken = generateCsrfToken(req, res);
res.render('login', {
users: MOCK_USERS,
error: req.query.error || null, // ошибка от предыдущего неудачного логина
csrfToken,
});
});
// POST /login — обработка формы входа.
// doubleCsrfProtection идёт первым: если CSRF-токен невалиден — отдаёт 403
// ещё до того как мы начнём работать с req.body.
router.post('/login', doubleCsrfProtection, async (req, res) => {
// Ищем выбранного пользователя по id ('admin', 'test', 'client2')
const user = MOCK_USERS.find(u => u.id === req.body.user);
if (!user) {
// Неизвестный id — скорее всего попытка подделки запроса
return res.redirect('/login?error=' + encodeURIComponent('Пользователь не найден'));
}
try {
// Выпускаем JWT с clientId, companyId, companyName, email.
// В продакшене этот токен приходит от Keycloak — здесь мы его имитируем.
const token = await auth.issue({
clientId: user.clientId,
companyId: user.companyId,
companyName: user.companyName,
email: user.email,
});
// Кладём токен в httpOnly cookie — JS на странице его не видит (защита от XSS).
// sameSite: lax — cookie отправляется при навигации, но не при кросс-сайтовых запросах.
res.cookie('jwt', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // только HTTPS в проде
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 часа в миллисекундах
});
// returnTo — параметр от auth.middleware: куда перенаправить после логина.
// Если не задан — на главную.
res.redirect(req.query.returnTo || '/');
} catch (e) {
console.error('Login error:', e);
res.redirect('/login?error=' + encodeURIComponent('Ошибка выпуска токена'));
}
});
// GET /logout — удаляем cookie и отправляем на /login.
// Простой GET (а не POST) допустим для мок-режима.
// В продакшене логаут должен быть POST с CSRF-токеном + редирект к Keycloak.
router.get('/logout', (req, res) => {
res.clearCookie('jwt');
res.redirect('/login');
});
return router;
}
module.exports = { createRouter };