import 'dotenv/config' import express from 'express' import cors from 'cors' import multer from 'multer' import { existsSync, mkdirSync, unlinkSync } from 'node:fs' import { basename, dirname, extname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { randomUUID } from 'node:crypto' import { authMiddleware, loginHandler, meHandler } from './auth.js' import { readDb, updateDb } from './db.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const UPLOADS_DIR = join(__dirname, '..', 'uploads') const PORT = Number(process.env.PORT) || 4000 if (!existsSync(UPLOADS_DIR)) mkdirSync(UPLOADS_DIR, { recursive: true }) const storage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, UPLOADS_DIR), filename: (_req, file, cb) => { const ext = extname(file.originalname) cb(null, `${randomUUID()}${ext}`) }, }) const MAX_UPLOAD_BYTES = Number(process.env.MAX_UPLOAD_MB || 200) * 1024 * 1024 const upload = multer({ storage, limits: { fileSize: MAX_UPLOAD_BYTES }, fileFilter: (_req, file, cb) => { const allowed = file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/') || file.mimetype === 'application/pdf' || file.mimetype === 'image/svg+xml' if (allowed) return cb(null, true) cb(new Error('فقط تصویر، ویدیو یا PDF مجاز است')) }, }) const app = express() const corsOrigin = process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173'] app.use(cors({ origin: corsOrigin, credentials: true })) app.use(express.json({ limit: '10mb' })) app.use('/uploads', express.static(UPLOADS_DIR)) app.post('/api/auth/login', loginHandler) app.get('/api/auth/me', authMiddleware, meHandler) app.get('/api/site', (_req, res) => { res.json(readDb().site) }) app.put('/api/site', authMiddleware, (req, res) => { const site = updateDb((db) => { db.site = req.body }).site res.json(site) }) app.get('/api/navigation', (_req, res) => { res.json(readDb().navigation) }) app.put('/api/navigation', authMiddleware, (req, res) => { const navigation = updateDb((db) => { db.navigation = req.body }).navigation res.json(navigation) }) app.get('/api/portfolio/categories', (_req, res) => { res.json(readDb().portfolioCategories) }) app.get('/api/portfolio/projects', (_req, res) => { res.json(readDb().projects) }) app.get('/api/portfolio/projects/:id', (req, res) => { const project = readDb().projects.find((p) => p.id === req.params.id) if (!project) return res.status(404).json({ error: 'پروژه یافت نشد' }) res.json(project) }) app.post('/api/portfolio/projects', authMiddleware, (req, res) => { const project = { id: randomUUID(), ...req.body } updateDb((db) => { db.projects.push(project) }) res.status(201).json(project) }) app.put('/api/portfolio/projects/:id', authMiddleware, (req, res) => { let updated = null updateDb((db) => { const i = db.projects.findIndex((p) => p.id === req.params.id) if (i === -1) return db.projects[i] = { ...db.projects[i], ...req.body, id: req.params.id } updated = db.projects[i] }) if (!updated) return res.status(404).json({ error: 'پروژه یافت نشد' }) res.json(updated) }) app.delete('/api/portfolio/projects/:id', authMiddleware, (req, res) => { let found = false updateDb((db) => { const i = db.projects.findIndex((p) => p.id === req.params.id) if (i !== -1) { db.projects.splice(i, 1) found = true } }) if (!found) return res.status(404).json({ error: 'پروژه یافت نشد' }) res.status(204).end() }) app.get('/api/services', (_req, res) => { res.json(readDb().services) }) app.put('/api/services', authMiddleware, (req, res) => { const services = updateDb((db) => { db.services = req.body }).services res.json(services) }) app.get('/api/services/:id', (req, res) => { const service = readDb().services.find((s) => s.id === req.params.id) if (!service) return res.status(404).json({ error: 'خدمت یافت نشد' }) res.json(service) }) app.put('/api/services/:id', authMiddleware, (req, res) => { let updated = null updateDb((db) => { const i = db.services.findIndex((s) => s.id === req.params.id) if (i === -1) return db.services[i] = { ...db.services[i], ...req.body, id: req.params.id } updated = db.services[i] }) if (!updated) return res.status(404).json({ error: 'خدمت یافت نشد' }) res.json(updated) }) app.get('/api/clients', (_req, res) => { res.json(readDb().clients) }) app.put('/api/clients', authMiddleware, (req, res) => { const clients = updateDb((db) => { db.clients = req.body }).clients res.json(clients) }) app.get('/api/stats', (_req, res) => { res.json(readDb().stats || []) }) app.get('/api/blog/posts', (_req, res) => { res.json(readDb().blogPosts.filter((p) => p.published)) }) app.get('/api/blog/posts/all', authMiddleware, (_req, res) => { res.json(readDb().blogPosts) }) app.get('/api/blog/posts/slug/:slug', (req, res) => { const post = readDb().blogPosts.find( (p) => p.slug === req.params.slug && p.published ) if (!post) return res.status(404).json({ error: 'مطلب یافت نشد' }) res.json(post) }) app.get('/api/blog/posts/:id', (req, res) => { const post = readDb().blogPosts.find((p) => p.id === req.params.id) if (!post) return res.status(404).json({ error: 'مطلب یافت نشد' }) res.json(post) }) app.post('/api/blog/posts', authMiddleware, (req, res) => { const post = { id: randomUUID(), published: false, publishedAt: new Date().toISOString(), ...req.body, } updateDb((db) => { db.blogPosts.push(post) }) res.status(201).json(post) }) app.put('/api/blog/posts/:id', authMiddleware, (req, res) => { let updated = null updateDb((db) => { const i = db.blogPosts.findIndex((p) => p.id === req.params.id) if (i === -1) return db.blogPosts[i] = { ...db.blogPosts[i], ...req.body, id: req.params.id } updated = db.blogPosts[i] }) if (!updated) return res.status(404).json({ error: 'مطلب یافت نشد' }) res.json(updated) }) app.delete('/api/blog/posts/:id', authMiddleware, (req, res) => { let found = false updateDb((db) => { const i = db.blogPosts.findIndex((p) => p.id === req.params.id) if (i !== -1) { db.blogPosts.splice(i, 1) found = true } }) if (!found) return res.status(404).json({ error: 'مطلب یافت نشد' }) res.status(204).end() }) app.get('/api/media', authMiddleware, (_req, res) => { res.json(readDb().media) }) app.post('/api/media/upload', authMiddleware, upload.single('file'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'فایلی ارسال نشده' }) const item = { id: randomUUID(), filename: req.file.originalname, url: `/uploads/${req.file.filename}`, mimeType: req.file.mimetype, size: req.file.size, createdAt: new Date().toISOString(), } updateDb((db) => { db.media.unshift(item) }) res.status(201).json(item) }) app.put('/api/media/:id', authMiddleware, (req, res) => { const { filename } = req.body || {} if (!filename?.trim()) return res.status(400).json({ error: 'نام فایل الزامی است' }) let updated = null updateDb((db) => { const i = db.media.findIndex((m) => m.id === req.params.id) if (i === -1) return db.media[i] = { ...db.media[i], filename: filename.trim() } updated = db.media[i] }) if (!updated) return res.status(404).json({ error: 'فایل یافت نشد' }) res.json(updated) }) app.post('/api/contact-requests', (req, res) => { const { name, email, phone, category, message } = req.body || {} if (!name?.trim() || !email?.trim() || !category?.trim() || !message?.trim()) { return res.status(400).json({ error: 'نام، ایمیل، نوع پروژه و توضیحات الزامی است' }) } const request = { id: randomUUID(), name: name.trim(), email: email.trim(), phone: phone?.trim() || '', category: category.trim(), message: message.trim(), status: 'new', createdAt: new Date().toISOString(), } updateDb((db) => { if (!Array.isArray(db.contactRequests)) db.contactRequests = [] db.contactRequests.unshift(request) }) res.status(201).json({ ok: true, id: request.id }) }) app.get('/api/contact-requests', authMiddleware, (_req, res) => { const requests = readDb().contactRequests || [] res.json(requests) }) app.patch('/api/contact-requests/:id', authMiddleware, (req, res) => { const { status } = req.body || {} if (!['new', 'read', 'archived'].includes(status)) { return res.status(400).json({ error: 'وضعیت نامعتبر است' }) } let updated = null updateDb((db) => { if (!Array.isArray(db.contactRequests)) db.contactRequests = [] const i = db.contactRequests.findIndex((r) => r.id === req.params.id) if (i === -1) return db.contactRequests[i] = { ...db.contactRequests[i], status } updated = db.contactRequests[i] }) if (!updated) return res.status(404).json({ error: 'درخواست یافت نشد' }) res.json(updated) }) app.delete('/api/contact-requests/:id', authMiddleware, (req, res) => { let found = false updateDb((db) => { if (!Array.isArray(db.contactRequests)) db.contactRequests = [] const i = db.contactRequests.findIndex((r) => r.id === req.params.id) if (i !== -1) { db.contactRequests.splice(i, 1) found = true } }) if (!found) return res.status(404).json({ error: 'درخواست یافت نشد' }) res.status(204).end() }) app.delete('/api/media/:id', authMiddleware, (req, res) => { let removed = null updateDb((db) => { const i = db.media.findIndex((m) => m.id === req.params.id) if (i === -1) return removed = db.media.splice(i, 1)[0] }) if (!removed) return res.status(404).json({ error: 'فایل یافت نشد' }) const filePath = join(UPLOADS_DIR, basename(removed.url)) if (existsSync(filePath)) unlinkSync(filePath) res.status(204).end() }) app.use((err, _req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: `حجم فایل بیش از حد مجاز است (حداکثر ${Number(process.env.MAX_UPLOAD_MB || 200)} مگابایت)`, }) } return res.status(400).json({ error: err.message }) } if (err) { return res.status(400).json({ error: err.message || 'خطای آپلود' }) } next() }) app.listen(PORT, () => { console.log(`Noro CMS API running at http://localhost:${PORT}`) })