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
+15 -3
View File
@@ -47,11 +47,23 @@ const upload = multer({
},
});
/* Personal image upload (Мои материалы): image-only, no library permission. */
/* Personal image upload (Мои материалы): image-only, no library permission.
* Stored in uploads/materials and served statically (public) so the saved
* <img>/open/download URL works without an auth header — these are personal
* study clips (board crops, drawings, textbook regions), not gated files. */
const MATERIALS_DIR = path.join(UPLOADS_DIR, 'materials');
try { require('fs').mkdirSync(MATERIALS_DIR, { recursive: true }); } catch (e) { /* exists */ }
const IMG_MIME = ['image/png','image/jpeg','image/gif','image/webp'];
const IMG_EXT = new Set(['.png','.jpg','.jpeg','.gif','.webp']);
const materialStorage = multer.diskStorage({
destination: MATERIALS_DIR,
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname || '') || '.png';
cb(null, require('crypto').randomBytes(16).toString('hex') + ext);
},
});
const imageUpload = multer({
storage,
storage: materialStorage,
limits: { fileSize: 20 * 1024 * 1024 }, // 20 MB
fileFilter: (_req, file, cb) => {
const ext = path.extname(file.originalname || '').toLowerCase();
@@ -65,7 +77,7 @@ router.use(authMiddleware);
router.get('/', ctrl.listFiles);
router.post('/', requireRole('teacher','admin'), requirePermission('library.upload'), upload.single('file'), fixUtf8Name, ctrl.uploadFile);
// Personal materials upload — any authenticated user (covered by router-level authMiddleware)
router.post('/personal', imageUpload.single('file'), fixUtf8Name, ctrl.uploadPersonalFile);
router.post('/personal', imageUpload.single('file'), ctrl.uploadPersonalFile);
/* ── folder routes (must be before /:id to avoid conflicts) ── */
router.get('/folders', ctrl.listFolders);