fix(photo-ai): филиальная изоляция в опросе и удалении превью
Два хендлера фото-воркера работали с requireAuth, но без entryAccessible,
в отличие от соседних /apply, /reject, /rollback. Тьютор одного филиала мог
доставать данные job'а записи другого филиала.
Закрыто:
- GET /api/entries/:id/photo/enhance-ai/:jobId отдавал after_path чужой
записи; добавлена проверка entryAccessible -> 403
- DELETE /api/entries/:id/photo/enhance-ai/preview вызывал safeUnlink(p)
по пути из тела запроса, проверенному только синтаксически через
isSafeUploadPath; теперь путь сначала сверяется с photo_jobs по
(entry_id, after_path, status='done', applied=false) и файл удаляется
только при реальном совпадении, иначе 404
Дополнительно:
- оба хендлера обёрнуты в try/catch: без глобального error handler
необработанная ошибка в async-роуте оставляла бы запрос висеть
- в DELETE добавлен logAudit('entry.photo.reject'), которого не было;
метка уже есть в public/js/audit.js
Проверено: изолированный тест на два филиала (15 проверок) — 403 для
чужого филиала на обоих эндпоинтах, happy-path владельца сохранён
(чтение job и удаление превью, job -> rejected), соседний /jobs без
регрессий; api.smoketest.js 76 PASS без регрессий.
Замечание для деплоя: server.js запечён в образ, compose монтирует
только uploads/, поэтому restart не подхватит правку — нужен
./scripts/deploy.sh или docker compose up -d --build app.
This commit is contained in:
@@ -6159,6 +6159,12 @@ app.post('/api/entries/:id/photo/enhance-ai', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
app.get('/api/entries/:id/photo/enhance-ai/:jobId', requireAuth, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
const acc = await entryAccessible(req.user, req.params.id);
|
||||
if (!acc.found) return res.status(404).json({ error: 'Not found' });
|
||||
if (!acc.allowed) return res.status(403).json({ error: 'Нет доступа к этой записи' });
|
||||
}
|
||||
const jobId = parseInt(req.params.jobId, 10);
|
||||
if (!Number.isFinite(jobId)) return res.status(404).json({ error: 'Задание не найдено' });
|
||||
const { rows } = await pool.query(
|
||||
@@ -6171,6 +6177,10 @@ app.get('/api/entries/:id/photo/enhance-ai/:jobId', requireAuth, async (req, res
|
||||
if (job.status === 'error') out.error = job.error;
|
||||
if (job.status === 'done') out.photo_path = job.after_path;
|
||||
res.json(out);
|
||||
} catch (e) {
|
||||
console.error('GET /api/entries/:id/photo/enhance-ai/:jobId:', e);
|
||||
res.status(500).json({ error: 'Ошибка получения статуса задания' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/entries/:id/photo/jobs/:jobId/apply', requireAuth, async (req, res) => {
|
||||
@@ -6347,15 +6357,34 @@ app.post('/api/entries/:id/photo/jobs/:jobId/rollback', requireAuth, async (req,
|
||||
});
|
||||
|
||||
app.delete('/api/entries/:id/photo/enhance-ai/preview', requireAuth, async (req, res) => {
|
||||
if (req.user.role !== 'admin') {
|
||||
const acc = await entryAccessible(req.user, req.params.id);
|
||||
if (!acc.found) return res.status(404).json({ error: 'Not found' });
|
||||
if (!acc.allowed) return res.status(403).json({ error: 'Нет доступа к этой записи' });
|
||||
}
|
||||
try {
|
||||
const { path: p } = req.body || {};
|
||||
if (!isSafeUploadPath(p)) return res.status(400).json({ error: 'Некорректный путь' });
|
||||
safeUnlink(p);
|
||||
await pool.query(
|
||||
`UPDATE photo_jobs SET status = 'rejected', error = 'Отклонено пользователем', finished_at = now()
|
||||
WHERE entry_id = $1 AND after_path = $2 AND status = 'done' AND applied = false`,
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, after_path FROM photo_jobs
|
||||
WHERE entry_id = $1 AND after_path = $2 AND status = 'done' AND applied = false
|
||||
ORDER BY id DESC LIMIT 1`,
|
||||
[req.params.id, p]
|
||||
);
|
||||
const job = rows[0];
|
||||
if (!job) return res.status(404).json({ error: 'Результат не найден' });
|
||||
if (isSafeUploadPath(job.after_path)) safeUnlink(job.after_path);
|
||||
await pool.query(
|
||||
`UPDATE photo_jobs SET status = 'rejected', error = 'Отклонено пользователем', finished_at = now()
|
||||
WHERE id = $1 AND applied = false`,
|
||||
[job.id]
|
||||
);
|
||||
await logAudit(req, 'entry.photo.reject', { entry_id: req.params.id, job_id: job.id });
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error('DELETE /api/entries/:id/photo/enhance-ai/preview:', e);
|
||||
res.status(500).json({ error: 'Ошибка удаления результата' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/entries/:id/photo/restore-original', requireAuth, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user