/** * A.1–A.4: локальный bcrypt (dev) и HR (HR_AUTH=1 + Werkzeug + staff_id) */ import express from 'express'; import { query } from '../db/db.js'; import { comparePassword, generateToken } from '../utils/auth.js'; import { authenticate } from '../middleware/auth.js'; import { queryHr, getHrPool } from '../db/hrPool.js'; import { mapHrRoleToApp } from '../utils/hrRoleMap.js'; import { isHrAuthEnabled, HR_MANAGED_PASSWORD_PLACEHOLDER, } from '../config/authConstants.js'; import { RU } from '../messages/ru.js'; import { getAssignmentDirectory, getHrDepartmentNames, } from '../services/assignmentDirectoryService.js'; import { isAssignmentFeatureEnabled } from '../config/featureFlags.js'; const router = express.Router(); router.post('/login', async (req, res) => { try { const { login, password } = req.body; if (!login || !password) { return res.status(400).json({ error: RU.loginAndPasswordRequired }); } if (isHrAuthEnabled()) { if (!getHrPool()) { return res.status(500).json({ error: RU.hrDatabaseUrlMissing }); } const u = await queryHr( `SELECT id, username, password_hash, role FROM users WHERE LOWER(TRIM(username)) = LOWER(TRIM($1))`, [login] ); if (u.rows.length === 0 || !u.rows[0].password_hash) { return res.status(401).json({ error: RU.invalidCredentials }); } const row = u.rows[0]; const ok = await comparePassword(password, row.password_hash); if (!ok) { return res.status(401).json({ error: RU.invalidCredentials }); } const s = await queryHr( `SELECT id, fio FROM staff_members WHERE LOWER(TRIM(COALESCE(web_login, ''))) = LOWER(TRIM($1))`, [login] ); if (s.rows.length === 0) { return res.status(403).json({ error: RU.noStaffForLogin }); } const staffId = s.rows[0].id; const fio = s.rows[0].fio || login; const appRole = mapHrRoleToApp(row.role); const up = await query( `INSERT INTO users (login, password_hash, full_name, role, department_id, is_active, staff_id) VALUES ($1, $2, $3, $4, null, true, $5) ON CONFLICT (staff_id) DO UPDATE SET login = EXCLUDED.login, full_name = EXCLUDED.full_name, role = EXCLUDED.role, password_hash = EXCLUDED.password_hash RETURNING id, login, full_name, role, department_id, staff_id`, [login, HR_MANAGED_PASSWORD_PLACEHOLDER, fio, appRole, staffId] ); const uu = up.rows[0]; const token = generateToken( uu.id, uu.role, uu.department_id, { staffId: uu.staff_id } ); res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000, }); return res.json({ user: { id: uu.id, login: uu.login, fullName: uu.full_name, role: uu.role, departmentId: uu.department_id, staffId: uu.staff_id, }, }); } const result = await query( 'SELECT id, login, password_hash, full_name, role, department_id FROM users WHERE login = $1 AND is_active = true', [login] ); if (result.rows.length === 0) { return res.status(401).json({ error: RU.invalidCredentials }); } const user = result.rows[0]; if (user.password_hash === HR_MANAGED_PASSWORD_PLACEHOLDER) { return res.status(401).json({ error: RU.useHrLogin }); } const isValidPassword = await comparePassword(password, user.password_hash); if (!isValidPassword) { return res.status(401).json({ error: RU.invalidCredentials }); } const token = generateToken(user.id, user.role, user.department_id); res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000, }); return res.json({ user: { id: user.id, login: user.login, fullName: user.full_name, role: user.role, departmentId: user.department_id, staffId: null, }, }); } catch (error) { if (error.message?.includes('HR database not configured')) { return res.status(500).json({ error: RU.hrDatabaseNotConfigured }); } console.error('Login error:', error); return res.status(500).json({ error: RU.loginFailed }); } }); router.post('/logout', (req, res) => { try { res.clearCookie('token', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', }); res.json({ message: RU.loggedOut }); } catch (error) { console.error('Logout error:', error); res.status(500).json({ error: RU.logoutFailed }); } }); router.get('/me', authenticate, async (req, res) => { try { const devUi = process.env.NODE_ENV === 'development'; const assignmentUi = isAssignmentFeatureEnabled(); res.json({ user: req.user, devUi, assignmentUi }); } catch (error) { console.error('Get current user error:', error); res.status(500).json({ error: RU.userDataFailed }); } }); /** * Каталог сотрудников для назначения: HR (все) + отделы + поиск. Как `POST .../assign`: см. `isAssignmentFeatureEnabled()`. * Query: q, department (имя отдела или __all__), clinic=all|with|without */ router.get('/dev/assignment-directory', authenticate, async (req, res) => { if (!isAssignmentFeatureEnabled()) { return res.status(404).json({ error: RU.notFound }); } try { const q = typeof req.query.q === 'string' ? req.query.q : ''; const department = typeof req.query.department === 'string' ? req.query.department : ''; const c = req.query.clinic; const clinicFilter = c === 'with' || c === 'without' ? c : 'all'; const { people, source } = await getAssignmentDirectory({ q, department, clinicFilter, }); const departments = await getHrDepartmentNames(); res.json({ people, source, departments }); } catch (error) { console.error('dev assignment directory:', error); res.status(500).json({ error: RU.userDataFailed }); } }); export default router;