Initial commit: API and deploy configs
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
PORT=4000
|
||||
NODE_ENV=production
|
||||
JWT_SECRET=change-this-to-a-long-random-secret
|
||||
ADMIN_EMAIL=admin@studionoro.ir
|
||||
ADMIN_PASSWORD=change-this-strong-password
|
||||
API_PUBLIC_URL=https://api.studionoro.ir
|
||||
CORS_ORIGIN=https://studionoro.ir,https://www.studionoro.ir,https://admin.studionoro.ir
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.env
|
||||
data/
|
||||
uploads/
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "studionoro-api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js",
|
||||
"seed": "node src/seed.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { readDb } from './db.js'
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret'
|
||||
|
||||
export function authMiddleware(req, res, next) {
|
||||
const header = req.headers.authorization
|
||||
if (!header?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(header.slice(7), JWT_SECRET)
|
||||
next()
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
|
||||
export function loginHandler(req, res) {
|
||||
const { email, password } = req.body || {}
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@norostudio.ir'
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'
|
||||
|
||||
if (email !== adminEmail || password !== adminPassword) {
|
||||
return res.status(401).json({ error: 'ایمیل یا رمز عبور نادرست است' })
|
||||
}
|
||||
|
||||
const token = jwt.sign({ email, role: 'admin' }, JWT_SECRET, { expiresIn: '7d' })
|
||||
res.json({ token, user: { email, role: 'admin' } })
|
||||
}
|
||||
|
||||
export function meHandler(req, res) {
|
||||
res.json({ user: req.user })
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const DATA_DIR = join(__dirname, '..', 'data')
|
||||
const DB_PATH = join(DATA_DIR, 'content.json')
|
||||
|
||||
const defaultDb = () => ({
|
||||
site: {},
|
||||
navigation: [],
|
||||
portfolioCategories: [],
|
||||
projects: [],
|
||||
services: [],
|
||||
serviceProjectCategories: {},
|
||||
clients: [],
|
||||
stats: [],
|
||||
blogPosts: [],
|
||||
media: []
|
||||
})
|
||||
|
||||
function ensureDataDir() {
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
export function readDb() {
|
||||
ensureDataDir()
|
||||
if (!existsSync(DB_PATH)) {
|
||||
const db = defaultDb()
|
||||
writeDb(db)
|
||||
return db
|
||||
}
|
||||
return JSON.parse(readFileSync(DB_PATH, 'utf-8'))
|
||||
}
|
||||
|
||||
export function writeDb(db) {
|
||||
ensureDataDir()
|
||||
writeFileSync(DB_PATH, JSON.stringify(db, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
export function updateDb(mutator) {
|
||||
const db = readDb()
|
||||
mutator(db)
|
||||
writeDb(db)
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import 'dotenv/config'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import multer from 'multer'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { 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 upload = multer({ storage })
|
||||
|
||||
const app = express()
|
||||
const corsOrigin = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()) || [
|
||||
'http://localhost:5173',
|
||||
'http://localhost:3000',
|
||||
]
|
||||
|
||||
app.use(cors({ origin: corsOrigin, credentials: true }))
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
app.use('/uploads', express.static(UPLOADS_DIR))
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ ok: true, service: 'studionoro-api' })
|
||||
})
|
||||
|
||||
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.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`Studio Noro API running at http://127.0.0.1:${PORT}`)
|
||||
})
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { existsSync, mkdirSync, copyFileSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { writeDb } from './db.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const UPLOADS = join(__dirname, '..', 'uploads')
|
||||
const NUXT = join(__dirname, '..', '..', 'nuxt-tailwind-app')
|
||||
|
||||
function asset(rel, name) {
|
||||
const src = join(NUXT, 'src', 'assets', 'images', rel)
|
||||
if (!existsSync(UPLOADS)) mkdirSync(UPLOADS, { recursive: true })
|
||||
if (existsSync(src)) {
|
||||
copyFileSync(src, join(UPLOADS, name))
|
||||
return `/uploads/${name}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function runSeed() {
|
||||
const cover = asset('portfolio/madar-biscuit-poster.png', 'portfolio-cover.png')
|
||||
|
||||
const services = JSON.parse(
|
||||
readFileSync(join(__dirname, 'services.seed.json'), 'utf-8')
|
||||
).map((s) => ({
|
||||
...s,
|
||||
iconUrl: asset(`services/${s.id === 'ui' ? 'ui' : s.id === 'graphic-design' ? 'graphic-design' : s.id === 'brand-identity' ? 'brand-identity' : s.id === 'social' ? 'social' : s.id === 'photography' ? 'photography' : 'video'}.png`, `icon-${s.id}.png`),
|
||||
backgroundUrl: asset(`services/backgrounds/${s.id === 'ui' ? 'ui' : s.id === 'graphic-design' ? 'graphic-design' : s.id === 'brand-identity' ? 'brand-identity' : s.id === 'social' ? 'social' : s.id === 'photography' ? 'photography' : 'video'}.png`, `bg-${s.id}.png`),
|
||||
portfolioCategorySlugs:
|
||||
{
|
||||
ui: ['uiux'],
|
||||
'graphic-design': ['graphic'],
|
||||
'brand-identity': ['logo', 'identity'],
|
||||
social: ['social'],
|
||||
photography: [],
|
||||
video: ['motion']
|
||||
}[s.id] || []
|
||||
}))
|
||||
|
||||
writeDb({
|
||||
site: {
|
||||
name: 'استودیو نورو',
|
||||
nameEn: 'Noro Studio',
|
||||
tagline: 'استودیوی طراحی دیجیتال',
|
||||
description:
|
||||
'استودیو نورو با تمرکز بر هویت بصری، UI/UX، طراحی سوشال مدیا و محتوای دیجیتال، برند شما را به تجربهای مدرن و حرفهای تبدیل میکند.',
|
||||
email: 'hello@studionoro.ir',
|
||||
website: 'studionoro.ir',
|
||||
phone: '۰۹۳۳ ۲۰۶ ۱۶۶۷',
|
||||
phoneTel: '+989332061667',
|
||||
location: 'اصفهان، ایران',
|
||||
messengerHandle: '@noro_vision',
|
||||
messengersLabel: 'پیامرسانها و شبکههای اجتماعی',
|
||||
messengers: [
|
||||
{ id: 'telegram', name: 'تلگرام', url: 'https://t.me/noro_vision', iconUrl: asset('messengers/telegram.png', 'tg.png') },
|
||||
{ id: 'bale', name: 'بله', url: 'https://ble.ir/noro_vision', iconUrl: asset('messengers/bale.png', 'bale.png') },
|
||||
{ id: 'rubika', name: 'روبیکا', url: 'https://rubika.ir/noro_vision', iconUrl: asset('messengers/rubika.png', 'rubika.png') },
|
||||
{ id: 'instagram', name: 'اینستاگرام', url: 'https://www.instagram.com/noro.vision?igsh=MXF1NGE2NGMxOGxncA==', iconUrl: asset('messengers/instagram.svg', 'ig.svg') }
|
||||
],
|
||||
seo: {
|
||||
defaultTitle: 'استودیو نورو | Noro Studio',
|
||||
defaultDescription: 'استودیو نورو — طراحی هویت بصری، UI/UX، سوشال مدیا و محتوای دیجیتال'
|
||||
},
|
||||
hero: {
|
||||
darkImageUrl: asset('hero/studio-background.png', 'hero-dark.png'),
|
||||
lightImageUrl: asset('hero/studio-background-light.png', 'hero-light.png')
|
||||
}
|
||||
},
|
||||
navigation: [
|
||||
{ label: 'خانه', to: '/' },
|
||||
{ label: 'نمونهکارها', to: '/portfolio' },
|
||||
{ label: 'خدمات', to: '/services' },
|
||||
{ label: 'وبلاگ', to: '/blog' },
|
||||
{ label: 'تماس', to: '/contact' }
|
||||
],
|
||||
portfolioCategories: [
|
||||
{ slug: 'graphic', label: 'طراحی گرافیک' },
|
||||
{ slug: 'logo', label: 'طراحی لوگو' },
|
||||
{ slug: 'identity', label: 'هویت بصری' },
|
||||
{ slug: 'uiux', label: 'UI/UX' },
|
||||
{ slug: 'social', label: 'طراحی سوشال مدیا' },
|
||||
{ slug: 'motion', label: 'تدوین و موشن گرافیک' }
|
||||
],
|
||||
projects: [
|
||||
{ id: '1', title: 'طراحی پوستر تبلیغاتی بیسکویت مادر', category: 'graphic', description: 'این پوستر به سفارش شرکت بیسکویت مادر طراحی شده است.', longDescription: 'پروژه پوستر تبلیغاتی بیسکویت مادر با هدف معرفی خواص غذایی و ارزشهای برند طراحی شد.', year: 'خرداد ۱۴۰۴', featured: true, imageUrl: cover, galleryUrls: [cover, cover], client: 'بیسکویت مادر', deliverables: ['پوستر تبلیغاتی'], seoTitle: '', seoDescription: '' },
|
||||
{ id: '2', title: 'اپلیکیشن فینتک', category: 'uiux', description: 'طراحی رابط و تجربه کاربری اپ موبایل مالی', longDescription: 'طراحی کامل UI/UX اپلیکیشن موبایل فینتک.', year: '۱۴۰۴', featured: true, imageUrl: cover, galleryUrls: [cover], client: 'استارتاپ فینتک', deliverables: ['طراحی UI'], seoTitle: '', seoDescription: '' },
|
||||
{ id: '3', title: 'لوگو استارتاپ نوا', category: 'logo', description: 'طراحی لوگوتایپ مینیمال', longDescription: 'لوگوتایپ استارتاپ نوا با رویکرد مینیمال طراحی شد.', year: '۱۴۰۳', featured: true, imageUrl: cover, galleryUrls: [cover], client: 'استارتاپ نوا', deliverables: ['لوگوتایپ'], seoTitle: '', seoDescription: '' },
|
||||
{ id: '4', title: 'کمپین اینستاگرام', category: 'social', description: 'طراحی قالب و محتوای بصری', longDescription: 'کمپین بصری اینستاگرام برای فروش فصلی.', year: '۱۴۰۳', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'برند خردهفروشی', deliverables: ['قالب پست'], seoTitle: '', seoDescription: '' },
|
||||
{ id: '5', title: 'موشن معرفی محصول', category: 'motion', description: 'تدوین و موشن گرافیک', longDescription: 'ویدیوی موشن گرافیک ۶۰ ثانیهای.', year: '۱۴۰۳', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'برند لوازم خانگی', deliverables: ['انیمیشن'], seoTitle: '', seoDescription: '' },
|
||||
{ id: '6', title: 'پنل مدیریت SaaS', category: 'uiux', description: 'طراحی داشبورد SaaS', longDescription: 'طراحی داشبورد مدیریتی SaaS.', year: '۱۴۰۲', featured: false, imageUrl: cover, galleryUrls: [cover], client: 'پلتفرم SaaS', deliverables: ['داشبورد'], seoTitle: '', seoDescription: '' }
|
||||
],
|
||||
services,
|
||||
serviceProjectCategories: {
|
||||
ui: ['uiux'],
|
||||
'graphic-design': ['graphic'],
|
||||
'brand-identity': ['logo', 'identity'],
|
||||
social: ['social'],
|
||||
photography: [],
|
||||
video: ['motion']
|
||||
},
|
||||
clients: [
|
||||
{ id: 'chehelsotoun-souleh', name: 'چهلستون سوله اصفهان', logoUrl: asset('clients/chehelsotoun-souleh.png', 'c1.png') },
|
||||
{ id: 'montzar', name: 'منتظر', logoUrl: asset('clients/montzar.png', 'c2.png') },
|
||||
{ id: 'madar', name: 'مادر', logoUrl: asset('clients/madar.png', 'c3.png') },
|
||||
{ id: 'azad-khorasgan', name: 'دانشگاه آزاد اسلامی — واحد خوراسگان', logoUrl: asset('clients/azad-khorasgan.png', 'c4.png') },
|
||||
{ id: 'isfahan', name: 'اصفهان', logoUrl: asset('clients/isfahan.png', 'c5.png') },
|
||||
{ id: 'montech', name: 'منتک', logoUrl: asset('clients/montech.png', 'c6.png') },
|
||||
{ id: 'farshchin', name: 'فرشچین', logoUrl: asset('clients/farshchin.png', 'c7.png') },
|
||||
{ id: 'rouyesh', name: 'رویش — گروه توسعه نرمافزاری', logoUrl: asset('clients/rouyesh.png', 'c8.png') }
|
||||
],
|
||||
stats: [
|
||||
{ value: '۵۰+', label: 'پروژه انجامشده' },
|
||||
{ value: '۳۰+', label: 'مشتری راضی' },
|
||||
{ value: '۶', label: 'حوزه تخصصی' }
|
||||
],
|
||||
blogPosts: [],
|
||||
media: []
|
||||
})
|
||||
|
||||
console.log('Seed completed.')
|
||||
}
|
||||
|
||||
runSeed()
|
||||
@@ -0,0 +1,260 @@
|
||||
[
|
||||
{
|
||||
"id": "ui",
|
||||
"title": "طراحی رابط کاربری (UI/UX)",
|
||||
"subtitle": "طراحی رابط کاربری حرفهای برای وبسایت و اپلیکیشن",
|
||||
"description": "طراحی رابط کاربری برای وب، موبایل و محصولات دیجیتال با تمرکز بر تجربه کاربری.",
|
||||
"intro": "ما رابطهای کاربری زیبا، کاربردی و کاربرمحور طراحی میکنیم تا کاربران تجربهای روان و لذتبخش از تعامل با محصول شما داشته باشند. هدف ما تنها زیبایی نیست؛ بلکه ایجاد تجربهای است که به افزایش تعامل کاربران و رشد کسبوکار شما کمک کند.",
|
||||
"includes": [
|
||||
"تحقیق و تحلیل کاربران",
|
||||
"طراحی User Flow",
|
||||
"طراحی Wireframe",
|
||||
"طراحی رابط کاربری صفحات",
|
||||
"طراحی نسخه موبایل و دسکتاپ",
|
||||
"طراحی سیستم طراحی (Design System)",
|
||||
"پروتوتایپ تعاملی"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش نرخ تبدیل کاربران",
|
||||
"بهبود تجربه کاربری",
|
||||
"کاهش سردرگمی کاربران",
|
||||
"افزایش اعتماد به برند",
|
||||
"آمادهسازی محصول برای توسعه حرفهای"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "شناخت پروژه", "description": "بررسی اهداف، مخاطبان و نیازهای محصول." },
|
||||
{ "title": "طراحی ساختار", "description": "طراحی مسیرهای کاربری و وایرفریم صفحات." },
|
||||
{ "title": "طراحی بصری", "description": "طراحی رابط کاربری بر اساس هویت برند." },
|
||||
{ "title": "بازبینی و اصلاح", "description": "دریافت بازخورد و اعمال تغییرات." },
|
||||
{ "title": "تحویل نهایی", "description": "ارائه فایلهای طراحی و مستندات لازم." }
|
||||
],
|
||||
"deliverables": [
|
||||
"طراحی صفحات وبسایت",
|
||||
"داشبوردهای مدیریتی",
|
||||
"اپلیکیشنهای موبایل",
|
||||
"لندینگ پیجها",
|
||||
"سیستمهای نرمافزاری"
|
||||
],
|
||||
"faq": [
|
||||
{
|
||||
"question": "آیا طراحی برای موبایل هم انجام میشود؟",
|
||||
"answer": "بله، تمامی پروژهها به صورت Responsive طراحی میشوند."
|
||||
},
|
||||
{
|
||||
"question": "فایل نهایی در چه فرمتی تحویل داده میشود؟",
|
||||
"answer": "فایلهای Figma و تمامی Assetهای مورد نیاز تحویل داده میشوند."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "graphic-design",
|
||||
"title": "طراحی گرافیک",
|
||||
"subtitle": "خلق هویت بصری جذاب برای برندها",
|
||||
"description": "خلق اثرهای بصری حرفهای برای چاپ، تبلیغات، بستهبندی و کمپینهای برند.",
|
||||
"intro": "طراحی گرافیک تنها ساخت تصاویر زیبا نیست؛ بلکه ابزاری برای انتقال پیام برند و ایجاد ارتباط مؤثر با مخاطبان است.",
|
||||
"includes": [
|
||||
"طراحی پوستر",
|
||||
"طراحی بنر تبلیغاتی",
|
||||
"طراحی بستهبندی",
|
||||
"طراحی کاتالوگ و بروشور",
|
||||
"طراحی استوری و پست شبکههای اجتماعی",
|
||||
"طراحی اقلام چاپی"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش جذابیت برند",
|
||||
"انتقال بهتر پیام",
|
||||
"ایجاد تمایز در بازار",
|
||||
"افزایش اثربخشی کمپینهای تبلیغاتی"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "دریافت نیازمندیها", "description": "جمعآوری اطلاعات و اهداف پروژه." },
|
||||
{ "title": "تحقیق و ایدهپردازی", "description": "بررسی روندها و تولید مفاهیم اولیه." },
|
||||
{ "title": "طراحی اولیه", "description": "ارائه طرحهای اولیه برای بازبینی." },
|
||||
{ "title": "اصلاح و بهینهسازی", "description": "اعمال بازخورد و بهبود طرح." },
|
||||
{ "title": "تحویل نهایی", "description": "تحویل فایلهای نهایی آماده استفاده." }
|
||||
],
|
||||
"deliverables": ["پوستر", "بنر", "کاتالوگ", "بستهبندی", "طراحی کمپین تبلیغاتی"],
|
||||
"faq": [
|
||||
{
|
||||
"question": "آیا فایل قابل چاپ تحویل داده میشود؟",
|
||||
"answer": "بله، تمامی فایلها با استاندارد چاپ آماده میشوند."
|
||||
},
|
||||
{
|
||||
"question": "چند مرحله اصلاح وجود دارد؟",
|
||||
"answer": "بسته به پروژه، چند مرحله بازبینی در نظر گرفته میشود."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "brand-identity",
|
||||
"title": "هویت برند",
|
||||
"subtitle": "ساخت تصویری ماندگار برای برند شما",
|
||||
"description": "طراحی سیستم برند یکپارچه شامل لوگو، رنگ، تایپوگرافی و راهنمای هویت بصری.",
|
||||
"intro": "هویت برند مجموعهای از عناصر بصری و مفهومی است که شخصیت برند شما را شکل میدهد و باعث میشود در ذهن مخاطب ماندگار شوید.",
|
||||
"includes": [
|
||||
"طراحی لوگو",
|
||||
"طراحی پالت رنگی",
|
||||
"انتخاب تایپوگرافی",
|
||||
"طراحی آیکونها",
|
||||
"طراحی المانهای گرافیکی",
|
||||
"تدوین Brand Guideline"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش اعتبار برند",
|
||||
"ایجاد یکپارچگی بصری",
|
||||
"متمایز شدن از رقبا",
|
||||
"افزایش اعتماد مخاطبان"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "شناخت برند", "description": "درک ارزشها، مخاطب و اهداف برند." },
|
||||
{ "title": "تحلیل بازار و رقبا", "description": "بررسی فضای رقابتی و فرصتها." },
|
||||
{ "title": "طراحی مفاهیم اولیه", "description": "ارائه چند مفهوم بصری برای انتخاب." },
|
||||
{ "title": "توسعه هویت بصری", "description": "تکمیل لوگو، رنگ، تایپوگرافی و المانها." },
|
||||
{ "title": "تدوین راهنمای برند", "description": "مستندسازی قوانین استفاده از هویت." }
|
||||
],
|
||||
"deliverables": ["لوگو", "برندبوک", "کارت ویزیت", "ست اداری", "داراییهای دیجیتال برند"],
|
||||
"faq": [
|
||||
{
|
||||
"question": "آیا برندبوک هم ارائه میشود؟",
|
||||
"answer": "بله، راهنمای کامل استفاده از هویت بصری ارائه خواهد شد."
|
||||
},
|
||||
{
|
||||
"question": "اگر لوگوی قبلی داشته باشیم چه؟",
|
||||
"answer": "امکان بازطراحی یا توسعه هویت موجود وجود دارد."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "social",
|
||||
"title": "سوشال مدیا",
|
||||
"subtitle": "مدیریت و تولید محتوای حرفهای برای شبکههای اجتماعی",
|
||||
"description": "طراحی و تولید محتوای بصری برای اینستاگرام، لینکدین و پلتفرمهای اجتماعی.",
|
||||
"intro": "حضور مؤثر در شبکههای اجتماعی نیازمند برنامهریزی، طراحی و تولید محتوای هدفمند است. ما به برند شما کمک میکنیم تا ارتباطی مستمر و مؤثر با مخاطبان خود داشته باشد.",
|
||||
"includes": [
|
||||
"طراحی پست و استوری",
|
||||
"تدوین تقویم محتوایی",
|
||||
"طراحی قالب اختصاصی",
|
||||
"تولید محتوای بصری",
|
||||
"مشاوره استراتژی محتوا",
|
||||
"مدیریت کمپینهای شبکه اجتماعی"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش آگاهی از برند",
|
||||
"رشد تعامل کاربران",
|
||||
"جذب مخاطب هدف",
|
||||
"ایجاد هویت بصری منسجم"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "تحلیل وضعیت فعلی", "description": "بررسی حضور فعلی برند در شبکهها." },
|
||||
{ "title": "تدوین استراتژی محتوا", "description": "تعیین اهداف، لحن و برنامه محتوایی." },
|
||||
{ "title": "طراحی قالبها", "description": "ساخت قالبهای بصری یکپارچه." },
|
||||
{ "title": "تولید محتوا", "description": "طراحی و تولید پست، استوری و محتوای تعاملی." },
|
||||
{ "title": "انتشار و بهینهسازی", "description": "انتشار محتوا و تحلیل عملکرد." }
|
||||
],
|
||||
"deliverables": [
|
||||
"پست اینستاگرام",
|
||||
"استوری",
|
||||
"کاور لینکدین",
|
||||
"بنر کمپین",
|
||||
"ریلز و محتوای تعاملی"
|
||||
],
|
||||
"faq": [
|
||||
{
|
||||
"question": "برای چه پلتفرمهایی تولید محتوا انجام میشود؟",
|
||||
"answer": "اینستاگرام، لینکدین و سایر شبکههای اجتماعی."
|
||||
},
|
||||
{
|
||||
"question": "آیا استراتژی محتوا هم ارائه میشود؟",
|
||||
"answer": "بله، پیش از تولید محتوا برنامه محتوایی تدوین میشود."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "photography",
|
||||
"title": "عکاسی",
|
||||
"subtitle": "ثبت حرفهای محصولات و برندها",
|
||||
"description": "عکاسی حرفهای محصول، پرتره و محتوای بصری برای استفاده در برند و تبلیغات.",
|
||||
"intro": "تصاویر حرفهای یکی از مهمترین عوامل تأثیرگذاری بر تصمیم مخاطبان هستند. ما با عکاسی حرفهای به نمایش بهتر محصولات و خدمات شما کمک میکنیم.",
|
||||
"includes": [
|
||||
"عکاسی محصول",
|
||||
"عکاسی تبلیغاتی",
|
||||
"عکاسی صنعتی",
|
||||
"عکاسی برندینگ",
|
||||
"ویرایش و روتوش تصاویر"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش کیفیت نمایش محصولات",
|
||||
"جلب اعتماد مشتریان",
|
||||
"تولید محتوای حرفهای",
|
||||
"ارتقای تصویر برند"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "برنامهریزی پروژه", "description": "هماهنگی اهداف، سبک و زمانبندی." },
|
||||
{ "title": "آمادهسازی صحنه", "description": "چیدمان نور، پسزمینه و تجهیزات." },
|
||||
{ "title": "عکاسی", "description": "اجرای جلسه عکاسی حرفهای." },
|
||||
{ "title": "ویرایش تصاویر", "description": "رنگبندی، روتوش و بهینهسازی." },
|
||||
{ "title": "تحویل نهایی", "description": "تحویل تصاویر با کیفیت مناسب هر رسانه." }
|
||||
],
|
||||
"deliverables": [
|
||||
"تصاویر فروشگاهی",
|
||||
"عکسهای تبلیغاتی",
|
||||
"تصاویر وبسایت",
|
||||
"تصاویر شبکههای اجتماعی"
|
||||
],
|
||||
"faq": [
|
||||
{
|
||||
"question": "آیا ادیت تصاویر هم انجام میشود؟",
|
||||
"answer": "بله، تمامی تصاویر به صورت حرفهای ویرایش میشوند."
|
||||
},
|
||||
{
|
||||
"question": "امکان عکاسی در محل مشتری وجود دارد؟",
|
||||
"answer": "بله، در صورت نیاز پروژه در محل اجرا میشود."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "video",
|
||||
"title": "تدوین و محتوای ویدیویی",
|
||||
"subtitle": "روایت حرفهای داستان برند شما با ویدئو",
|
||||
"description": "تدوین، موشن گرافیک و تولید محتوای ویدیویی برای معرفی برند و کمپینها.",
|
||||
"intro": "ویدئو یکی از قدرتمندترین ابزارهای ارتباطی است. ما با تولید و تدوین محتوای ویدیویی حرفهای به برند شما کمک میکنیم تا پیام خود را تأثیرگذارتر منتقل کند.",
|
||||
"includes": [
|
||||
"تدوین ویدئو",
|
||||
"ساخت تیزر تبلیغاتی",
|
||||
"موشن گرافیک",
|
||||
"ویدئوهای شبکه اجتماعی",
|
||||
"اصلاح رنگ و صدا",
|
||||
"ساخت ویدئوهای معرفی برند"
|
||||
],
|
||||
"benefits": [
|
||||
"افزایش تعامل مخاطبان",
|
||||
"انتقال مؤثر پیام برند",
|
||||
"افزایش نرخ تبدیل",
|
||||
"ارتقای کیفیت محتوای دیجیتال"
|
||||
],
|
||||
"process": [
|
||||
{ "title": "دریافت نیاز پروژه", "description": "شناخت اهداف و مخاطب ویدئو." },
|
||||
{ "title": "سناریو و برنامهریزی", "description": "تدوین سناریو و استوریبورد." },
|
||||
{ "title": "تولید یا دریافت محتوا", "description": "فیلمبرداری یا دریافت فوتیج." },
|
||||
{ "title": "تدوین و اصلاح", "description": "تدوین، موشن و اصلاح رنگ و صدا." },
|
||||
{ "title": "خروجی نهایی", "description": "تحویل ویدئو در فرمتهای مورد نیاز." }
|
||||
],
|
||||
"deliverables": [
|
||||
"تیزر تبلیغاتی",
|
||||
"ویدئوی معرفی برند",
|
||||
"موشن گرافیک",
|
||||
"ریلز اینستاگرام",
|
||||
"ویدئوهای آموزشی"
|
||||
],
|
||||
"faq": [
|
||||
{
|
||||
"question": "خروجی برای شبکههای اجتماعی هم آماده میشود؟",
|
||||
"answer": "بله، ویدئوها متناسب با هر پلتفرم خروجی گرفته میشوند."
|
||||
},
|
||||
{
|
||||
"question": "آیا موشن گرافیک هم ارائه میشود؟",
|
||||
"answer": "بله، طراحی و اجرای موشن گرافیک بخشی از این خدمت است."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
*.log
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_ROOT="${APP_ROOT:-/var/www/studionoro}"
|
||||
|
||||
echo "==> Pull latest code (if using git)..."
|
||||
cd "$APP_ROOT"
|
||||
git pull || true
|
||||
|
||||
echo "==> Rebuild API..."
|
||||
cd "$APP_ROOT/api"
|
||||
npm ci --omit=dev || npm install --omit=dev
|
||||
|
||||
echo "==> Rebuild Nuxt..."
|
||||
cd "$APP_ROOT/nuxt-tailwind-app"
|
||||
npm ci || npm install
|
||||
npm run build
|
||||
|
||||
echo "==> Rebuild admin..."
|
||||
cd "$APP_ROOT/admin"
|
||||
npm ci || npm install
|
||||
npm run build
|
||||
|
||||
echo "==> Restart services..."
|
||||
cd "$APP_ROOT"
|
||||
pm2 restart ecosystem.config.cjs
|
||||
systemctl reload nginx
|
||||
|
||||
echo "Deploy complete."
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
NUXT_PUBLIC_CMS_API_URL=https://api.studionoro.ir
|
||||
NODE_ENV=production
|
||||
@@ -0,0 +1,84 @@
|
||||
# Nginx site config for studionoro.ir
|
||||
# Copy to: /etc/nginx/sites-available/studionoro
|
||||
# Enable: sudo ln -sf /etc/nginx/sites-available/studionoro /etc/nginx/sites-enabled/
|
||||
|
||||
upstream studionoro_web {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream studionoro_api {
|
||||
server 127.0.0.1:4000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# --- Main website (Nuxt) ---
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name studionoro.ir www.studionoro.ir;
|
||||
|
||||
client_max_body_size 20M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://studionoro_web;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
||||
# --- Admin panel (Vue static build) ---
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name admin.studionoro.ir;
|
||||
|
||||
root /var/www/studionoro/admin/dist;
|
||||
index index.html;
|
||||
client_max_body_size 20M;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://studionoro_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /uploads/ {
|
||||
proxy_pass http://studionoro_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
# --- API backend ---
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name api.studionoro.ir;
|
||||
|
||||
client_max_body_size 20M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://studionoro_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'studionoro-api',
|
||||
cwd: '/var/www/studionoro/api',
|
||||
script: 'src/index.js',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
max_memory_restart: '300M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 4000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'studionoro-web',
|
||||
cwd: '/var/www/studionoro/nuxt-tailwind-app',
|
||||
script: '.output/server/index.mjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
max_memory_restart: '512M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000,
|
||||
HOST: '127.0.0.1',
|
||||
NITRO_HOST: '127.0.0.1',
|
||||
NITRO_PORT: '3000',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Studio Noro — VPS bootstrap (Ubuntu 22.04/24.04)
|
||||
# Run as root or with sudo on a fresh VPS after uploading the project.
|
||||
|
||||
APP_ROOT="${APP_ROOT:-/var/www/studionoro}"
|
||||
DOMAIN="${DOMAIN:-studionoro.ir}"
|
||||
ADMIN_DOMAIN="${ADMIN_DOMAIN:-admin.studionoro.ir}"
|
||||
API_DOMAIN="${API_DOMAIN:-api.studionoro.ir}"
|
||||
|
||||
echo "==> Installing system packages..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl git nginx ufw certbot python3-certbot-nginx
|
||||
|
||||
echo "==> Installing Node.js 20 LTS..."
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
fi
|
||||
|
||||
echo "==> Installing PM2..."
|
||||
npm install -g pm2
|
||||
|
||||
echo "==> Configuring firewall..."
|
||||
ufw allow OpenSSH
|
||||
ufw allow 'Nginx Full'
|
||||
ufw --force enable
|
||||
|
||||
echo "==> Creating app directory..."
|
||||
mkdir -p "$APP_ROOT"
|
||||
|
||||
if [ ! -d "$APP_ROOT/api" ]; then
|
||||
echo "ERROR: Project files not found in $APP_ROOT"
|
||||
echo "Upload or clone your project to $APP_ROOT first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Installing API dependencies..."
|
||||
cd "$APP_ROOT/api"
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "WARNING: Edit $APP_ROOT/api/.env before going live (JWT_SECRET, ADMIN_PASSWORD)."
|
||||
fi
|
||||
npm ci --omit=dev || npm install --omit=dev
|
||||
|
||||
echo "==> Seeding API data (first run)..."
|
||||
npm run seed || true
|
||||
|
||||
echo "==> Building Nuxt frontend..."
|
||||
cd "$APP_ROOT/nuxt-tailwind-app"
|
||||
if [ ! -f .env ]; then
|
||||
cp "$APP_ROOT/deploy/env/nuxt.env.example" .env
|
||||
fi
|
||||
npm ci || npm install
|
||||
npm run build
|
||||
|
||||
echo "==> Building admin panel..."
|
||||
cd "$APP_ROOT/admin"
|
||||
npm ci || npm install
|
||||
npm run build
|
||||
|
||||
echo "==> Installing Nginx config..."
|
||||
cp "$APP_ROOT/deploy/nginx/studionoro.conf" /etc/nginx/sites-available/studionoro
|
||||
ln -sf /etc/nginx/sites-available/studionoro /etc/nginx/sites-enabled/studionoro
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
|
||||
echo "==> Starting PM2 apps..."
|
||||
cp "$APP_ROOT/deploy/pm2/ecosystem.config.cjs" "$APP_ROOT/ecosystem.config.cjs"
|
||||
cd "$APP_ROOT"
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
pm2 startup systemd -u root --hp /root || pm2 startup
|
||||
|
||||
echo "==> Requesting SSL certificates..."
|
||||
certbot --nginx \
|
||||
-d "$DOMAIN" -d "www.$DOMAIN" \
|
||||
-d "$ADMIN_DOMAIN" \
|
||||
-d "$API_DOMAIN" \
|
||||
--non-interactive --agree-tos -m "admin@$DOMAIN" || \
|
||||
echo "Certbot failed — set DNS first, then run certbot manually."
|
||||
|
||||
echo ""
|
||||
echo "Done. Verify:"
|
||||
echo " https://$DOMAIN"
|
||||
echo " https://$ADMIN_DOMAIN"
|
||||
echo " https://$API_DOMAIN/health"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copy project to VPS (run from Windows PowerShell in C:\Users\SMB\Projects)
|
||||
|
||||
# Option A — using SCP (replace VPS_IP and user):
|
||||
# scp -r api admin nuxt-tailwind-app deploy user@VPS_IP:/var/www/studionoro/
|
||||
|
||||
# Option B — using Git on VPS (recommended):
|
||||
# 1. Push this repo to GitHub/GitLab
|
||||
# 2. On VPS: git clone YOUR_REPO_URL /var/www/studionoro
|
||||
|
||||
# After files are on VPS:
|
||||
# ssh user@VPS_IP
|
||||
# cd /var/www/studionoro
|
||||
# chmod +x deploy/setup-vps.sh deploy/deploy.sh
|
||||
# sudo bash deploy/setup-vps.sh
|
||||
Reference in New Issue
Block a user