feat(card1): версии тестов API, черновик, HR-login, import, UI
- V.1–V.3: saveTestDraft, fork при попытках; миграция 003 staff_id - V.4–V.6: REST /api/tests, activate, PATCH, start attempt - A: HR_DATABASE_URL + Werkzeug/bcrypt, JWT staffId, HR_AUTH - D.1: multipart /api/tests/import/document - Frontend: login, список тестов, экран версий/черновика/попытки - ТЗ: V.10 назначения vs активная версия; журнал приёма Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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: 'Login and password are required' });
|
||||
}
|
||||
|
||||
if (isHrAuthEnabled()) {
|
||||
if (!getHrPool()) {
|
||||
return res.status(500).json({ error: 'HR_DATABASE_URL is not set' });
|
||||
}
|
||||
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: 'Invalid credentials' });
|
||||
}
|
||||
const row = u.rows[0];
|
||||
const ok = await comparePassword(password, row.password_hash);
|
||||
if (!ok) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
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: 'No staff link for this login (web_login)' });
|
||||
}
|
||||
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: 'Invalid credentials' });
|
||||
}
|
||||
const user = result.rows[0];
|
||||
if (user.password_hash === HR_MANAGED_PASSWORD_PLACEHOLDER) {
|
||||
return res.status(401).json({ error: 'Use HR login' });
|
||||
}
|
||||
const isValidPassword = await comparePassword(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
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: 'HR database not configured' });
|
||||
}
|
||||
console.error('Login error:', error);
|
||||
return res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
try {
|
||||
res.clearCookie('token', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/me', authenticate, async (req, res) => {
|
||||
try {
|
||||
res.json({ user: req.user });
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ error: 'Failed to get user data' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user