fix(materials): картинки материалов отдаются публично (рендер/открытие/скачивание)

/api/files/:id/download требует Bearer-заголовок, поэтому <img>, переход по
ссылке и «Скачать» для сохранённых картинок ломались (битое изображение,
клик не открывал). Теперь личные картинки складываются в uploads/materials и
отдаются статикой (как flashcards): POST /api/files/personal возвращает
{ url:'/uploads/materials/<file>' }. board-clip, material-save, textbook-clip
и рисовалка в my-materials сохраняют этот публичный url.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 14:30:47 +03:00
parent 55c8c5fa51
commit 53e996e2e0
7 changed files with 25 additions and 24 deletions
+5 -17
View File
@@ -170,31 +170,19 @@ function uploadFile(req, res) {
/* ── POST /api/files/personal ────────────────────────────────────────────
* Personal image upload for «Мои материалы» (board crops, drawings, textbook
* clips). Allowed for ANY authenticated user (incl. students) — unlike the
* teacher library upload above. Image-only; stored is_public=1 so the
* material's download URL resolves (and survives teacher «раздача»). */
* teacher library upload above. Image-only; saved into uploads/materials and
* served statically (public), so the returned URL renders in <img>, opens in
* a new tab and downloads without an auth header. Returns { url }. */
function uploadPersonalFile(req, res) {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const filePath = path.resolve(UPLOADS_DIR, req.file.filename);
const filePath = path.resolve(UPLOADS_DIR, 'materials', req.file.filename);
if (!checkMagicBytes(filePath, req.file.mimetype)) {
try { fs.unlinkSync(filePath); } catch {}
return res.status(400).json({ error: 'Содержимое файла не соответствует его расширению' });
}
const title = (req.body && req.body.title && req.body.title.trim()) || req.file.originalname || 'Изображение';
let r;
try {
r = db.prepare(`
INSERT INTO files (title, original_name, stored_name, mimetype, size, is_public, uploaded_by)
VALUES (?, ?, ?, ?, ?, 1, ?)
`).run(title, req.file.originalname, req.file.filename, req.file.mimetype, req.file.size, req.user.id);
} catch (err) {
const fp = path.resolve(UPLOADS_DIR, req.file.filename);
if (fp.startsWith(UPLOADS_DIR + path.sep)) { try { fs.unlinkSync(fp); } catch {} }
throw err;
}
res.status(201).json({ id: r.lastInsertRowid });
res.status(201).json({ url: '/uploads/materials/' + req.file.filename });
}
/* ── GET /api/files/:id/download ─────────────────────────────────────── */