You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
6.2 KiB

/**
* 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;