Files
2026-08-13 12:11:32 +03:30

354 lines
10 KiB
JavaScript

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}`)
})