PR 3 of the in-place admin router split (docs/website/API_V2_PLAN.md § Phase 2). Moves the content tier out of the residual admin.routes.js into one router file per capability, each mounted at the prefix it already owned. No URL, gate or handler changes. posts.router.js ( 9) /admin/posts uploads.router.js ( 1) /admin/uploads wiki.router.js (14) /admin/wiki pages.router.js ( 7) /admin/pages admin.routes.js (33) residual, was 64 All four capabilities are editor tier, so no gate moved: the shared `noindex, isLoggedIn, staffOnly` in admin/index.js is their whole gate. The multer config moved to admin/imageUpload.js because the two routes that share it (POST /posts/upload and POST /uploads) now live in different files; duplicating a mimetype allowlist is how the two copies drift. It stays in admin/ because UPLOAD_DIR is resolved relative to __dirname. Acceptance — all four gates zero-diff: routes.manifest.json unchanged (200 public + 2 internal) routes.guards.json unchanged (no route lost or gained a gate) swagger-output.json unchanged (198 operations) api-route-inventory.json already in sync plus 434 server tests green. Verified separately, because no gate can catch it: the wiki router's literal /categories and /tags paths still precede /:slug in declaration order. The manifest sorts its entries, so a reordering there would be invisible. Co-Authored-By: Claude <noreply@anthropic.com>
50 lines
1.9 KiB
JavaScript
50 lines
1.9 KiB
JavaScript
// Shared multer middleware for the two admin image-upload routes:
|
|
// POST /admin/posts/upload (posts.router.js) and POST /admin/uploads
|
|
// (uploads.router.js). It lived inline in admin.routes.js while both routes did;
|
|
// the PR 3 split put them in different files, so the config moved here rather
|
|
// than being duplicated — one upload directory, one mimetype allowlist.
|
|
//
|
|
// Kept in this directory on purpose: UPLOAD_DIR is resolved relative to
|
|
// __dirname, so moving the file to another folder would silently repoint the
|
|
// upload directory.
|
|
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const crypto = require('crypto')
|
|
const multer = require('multer')
|
|
|
|
const UPLOAD_DIR =
|
|
process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads')
|
|
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
|
|
// Whitelisted image mimetypes → the extension we store the file under. The
|
|
// stored extension is derived from this map (keyed by the accepted mimetype),
|
|
// never from originalname — so a spoofed `Content-Type: image/png` paired with
|
|
// `originalname: x.html` can never land an executable .html file in /uploads.
|
|
const MIME_EXT = {
|
|
'image/png': '.png',
|
|
'image/jpeg': '.jpg',
|
|
'image/gif': '.gif',
|
|
'image/webp': '.webp',
|
|
'image/avif': '.avif',
|
|
}
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, UPLOAD_DIR),
|
|
filename: (req, file, cb) => {
|
|
const ext = MIME_EXT[file.mimetype] || ''
|
|
cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`)
|
|
},
|
|
})
|
|
const upload = multer({
|
|
storage,
|
|
limits: { fileSize: 8 * 1024 * 1024 },
|
|
fileFilter: (req, file, cb) => {
|
|
// Single source of truth: only mimetypes we can map to a safe extension pass.
|
|
if (MIME_EXT[file.mimetype]) cb(null, true)
|
|
else cb(new Error('Only image uploads are allowed'))
|
|
},
|
|
})
|
|
|
|
module.exports = { upload, UPLOAD_DIR, MIME_EXT }
|