0fe04d4d99
- Сервисы: testAttemptService, testAccess, document import/gen/extract, LLM, assignment, aiEditor - Конфиг: devAuthor, featureFlags; messages/ru; интеграция V.9 (skip без БД) - API/роуты: app, auth, server; Dockerfile и env example - Фронт: TestAttempt, TestAttemptReview, AttemptReviewBlock, стили, правки App/api/login/vite - compose и README; смоук-тесты расширены Закрывает отсутствие модулей в origin после клона. Made-with: Cursor
189 lines
6.2 KiB
JavaScript
189 lines
6.2 KiB
JavaScript
/**
|
||
* 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;
|