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