feat: полный бэк и фронт (попытки, разбор, импорт, ИИ, назначения)

- Сервисы: 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
This commit is contained in:
Константин Лебединский
2026-04-24 22:55:15 +05:00
parent a68331c86b
commit 0fe04d4d99
38 changed files with 3683 additions and 491 deletions
+49 -16
View File
@@ -11,6 +11,12 @@ 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();
@@ -18,12 +24,12 @@ 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' });
return res.status(400).json({ error: RU.loginAndPasswordRequired });
}
if (isHrAuthEnabled()) {
if (!getHrPool()) {
return res.status(500).json({ error: 'HR_DATABASE_URL is not set' });
return res.status(500).json({ error: RU.hrDatabaseUrlMissing });
}
const u = await queryHr(
`SELECT id, username, password_hash, role
@@ -32,12 +38,12 @@ router.post('/login', async (req, res) => {
[login]
);
if (u.rows.length === 0 || !u.rows[0].password_hash) {
return res.status(401).json({ error: 'Invalid credentials' });
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: 'Invalid credentials' });
return res.status(401).json({ error: RU.invalidCredentials });
}
const s = await queryHr(
`SELECT id, fio FROM staff_members
@@ -45,9 +51,7 @@ router.post('/login', async (req, res) => {
[login]
);
if (s.rows.length === 0) {
return res
.status(403)
.json({ error: 'No staff link for this login (web_login)' });
return res.status(403).json({ error: RU.noStaffForLogin });
}
const staffId = s.rows[0].id;
const fio = s.rows[0].fio || login;
@@ -93,15 +97,15 @@ router.post('/login', async (req, res) => {
[login]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
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: 'Use HR login' });
return res.status(401).json({ error: RU.useHrLogin });
}
const isValidPassword = await comparePassword(password, user.password_hash);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
return res.status(401).json({ error: RU.invalidCredentials });
}
const token = generateToken(user.id, user.role, user.department_id);
res.cookie('token', token, {
@@ -122,10 +126,10 @@ router.post('/login', async (req, res) => {
});
} catch (error) {
if (error.message?.includes('HR database not configured')) {
return res.status(500).json({ error: 'HR database not configured' });
return res.status(500).json({ error: RU.hrDatabaseNotConfigured });
}
console.error('Login error:', error);
return res.status(500).json({ error: 'Login failed' });
return res.status(500).json({ error: RU.loginFailed });
}
});
@@ -136,19 +140,48 @@ router.post('/logout', (req, res) => {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
});
res.json({ message: 'Logged out successfully' });
res.json({ message: RU.loggedOut });
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ error: 'Logout failed' });
res.status(500).json({ error: RU.logoutFailed });
}
});
router.get('/me', authenticate, async (req, res) => {
try {
res.json({ user: req.user });
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: 'Failed to get user data' });
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 });
}
});