feat: real file upload to /opt/hotelsync/uploads + serve via /uploads/ on API
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/jwt": "^8.0.1",
|
||||
"@fastify/multipart": "^9.4.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@fastify/websocket": "^8.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"fastify": "^4.28.1",
|
||||
|
||||
@@ -4,6 +4,9 @@ import cookie from '@fastify/cookie'
|
||||
import cors from '@fastify/cors'
|
||||
import helmet from '@fastify/helmet'
|
||||
import rateLimit from '@fastify/rate-limit'
|
||||
import multipart from '@fastify/multipart'
|
||||
import staticFiles from '@fastify/static'
|
||||
import { join } from 'path'
|
||||
import { config } from './config'
|
||||
import './types' // side-effect: augments fastify types
|
||||
|
||||
@@ -22,6 +25,7 @@ import hotelSettingsRoutes from './routes/hotel-settings'
|
||||
import rentalRoutes from './routes/rental'
|
||||
import categoriesRoutes from './routes/categories'
|
||||
import tariffsRoutes from './routes/tariffs'
|
||||
import uploadRoutes from './routes/upload'
|
||||
|
||||
export async function buildApp() {
|
||||
const fastify = Fastify({
|
||||
@@ -34,6 +38,11 @@ export async function buildApp() {
|
||||
bodyLimit: 20 * 1024 * 1024, // 20MB — for base64 photo uploads
|
||||
})
|
||||
|
||||
// ── File uploads & static ─────────────────────────────────────────────────
|
||||
await fastify.register(multipart)
|
||||
const uploadsDir = process.env.UPLOADS_DIR ?? join(process.cwd(), 'uploads')
|
||||
await fastify.register(staticFiles, { root: uploadsDir, prefix: '/uploads/' })
|
||||
|
||||
// ── Security ───────────────────────────────────────────────────────────────
|
||||
await fastify.register(helmet, { contentSecurityPolicy: false })
|
||||
|
||||
@@ -86,6 +95,7 @@ export async function buildApp() {
|
||||
await fastify.register(rentalRoutes)
|
||||
await fastify.register(categoriesRoutes)
|
||||
await fastify.register(tariffsRoutes)
|
||||
await fastify.register(uploadRoutes)
|
||||
|
||||
return fastify
|
||||
}
|
||||
|
||||
42
backend/src/routes/upload.ts
Normal file
42
backend/src/routes/upload.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
import { createWriteStream, mkdirSync } from 'fs'
|
||||
import { join, extname } from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { pipeline } from 'stream/promises'
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? join(process.cwd(), 'uploads')
|
||||
|
||||
// Ensure uploads directory exists
|
||||
mkdirSync(UPLOADS_DIR, { recursive: true })
|
||||
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
const upload: FastifyPluginAsync = async (fastify) => {
|
||||
// POST /api/upload — upload a single image, returns { url }
|
||||
fastify.post(
|
||||
'/api/upload',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
const data = await request.file({ limits: { fileSize: MAX_SIZE } })
|
||||
if (!data) return reply.code(400).send({ error: 'No file provided' })
|
||||
|
||||
if (!ALLOWED_TYPES.includes(data.mimetype)) {
|
||||
return reply.code(400).send({ error: 'Only images allowed (jpg, png, webp, gif)' })
|
||||
}
|
||||
|
||||
const ext = extname(data.filename) || '.jpg'
|
||||
const filename = `${randomUUID()}${ext}`
|
||||
const filepath = join(UPLOADS_DIR, filename)
|
||||
|
||||
await pipeline(data.file, createWriteStream(filepath))
|
||||
|
||||
const host = (request.headers['x-forwarded-proto'] ?? 'https') + '://' +
|
||||
(request.headers['x-forwarded-host'] ?? request.headers.host ?? 'api.hotelsync.ru')
|
||||
|
||||
return { url: `${host}/uploads/${filename}` }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default upload
|
||||
Reference in New Issue
Block a user