Spaces:
Runtime error
Runtime error
|
Download PHASE_2_BACKEND.md from moustafa7/flwo: direct link, hf CLI and curl.
- Browser
- Download file 54.5 kB
-
https://huggingface.co/spaces/moustafa7/flwo/resolve/main/PHASE_2_BACKEND.md
- Command line
-
hf download hf://spaces/moustafa7/flwo/PHASE_2_BACKEND.md
-
curl -L -o PHASE_2_BACKEND.md https://huggingface.co/spaces/moustafa7/flwo/resolve/main/PHASE_2_BACKEND.md
54.5 kB
DesignFlow โ ุงูู ุฑุญูุฉ ุงูุซุงููุฉ: Backend + PostgreSQL + Security
ููู AI ุงูู ููุฐ: ุงูุฑุฃ ูุฐุง ุงูู ูู ูุงู ูุงู ูุจู ูุชุงุจุฉ ุฃู ุณุทุฑ ููุฏ.
ูู ูุฑุงุฑ ุฃู ูู ููุง ุฅูุฒุงู ู ูููุณ ุงุฎุชูุงุฑูุงู. ุชุฑุชูุจ ุงูุชูููุฐ ู ูู .
ูุง ุชุจุฏุฃ ุฑุจุท ุงูู Frontend ุฅูุง ุจุนุฏ ุงุฎุชุจุงุฑ ูู endpoint ุจู Postman/Thunder Client.
1. ุฅุนุฏุงุฏ ุงูู ุดุฑูุน
mkdir apps/server && cd apps/server
npm init -y
# Core
npm install express cors helmet cookie-parser express-rate-limit
# Database
npm install prisma @prisma/client
npx prisma init
# Redis
npm install ioredis
# Auth & Security
npm install bcryptjs jsonwebtoken
# Validation
npm install zod
# File Upload
npm install multer
npm install cloudinary multer-storage-cloudinary
# Real-time
npm install socket.io
# Utilities
npm install uuid dotenv morgan compression
# Dev
npm install -D nodemon jest supertest
1.1 ูููู ู ุฌูุฏุงุช ุงูู Backend ุงููุงู ู
apps/server/
โโโ prisma/
โ โโโ schema.prisma
โ โโโ migrations/
โ โโโ (auto-generated)
โ
โโโ src/
โ โโโ config/
โ โ โโโ index.js โ env validation + config object
โ โ โโโ database.js โ Prisma singleton
โ โ โโโ redis.js โ ioredis singleton
โ โ
โ โโโ middleware/
โ โ โโโ auth.js โ verifyToken, requireRole
โ โ โโโ validate.js โ zod validation wrapper
โ โ โโโ rateLimiter.js โ rate limit factories
โ โ โโโ upload.js โ multer + cloudinary config
โ โ โโโ errorHandler.js โ global error handler
โ โ
โ โโโ modules/
โ โ โโโ auth/
โ โ โ โโโ auth.routes.js
โ โ โ โโโ auth.controller.js
โ โ โ โโโ auth.service.js
โ โ โ โโโ auth.schema.js
โ โ โโโ users/
โ โ โ โโโ users.routes.js
โ โ โ โโโ users.controller.js
โ โ โ โโโ users.service.js
โ โ โ โโโ users.schema.js
โ โ โโโ templates/
โ โ โ โโโ templates.routes.js
โ โ โ โโโ templates.controller.js
โ โ โ โโโ templates.service.js
โ โ โ โโโ templates.schema.js
โ โ โโโ orders/
โ โ โ โโโ orders.routes.js
โ โ โ โโโ orders.controller.js
โ โ โ โโโ orders.service.js
โ โ โ โโโ orders.schema.js
โ โ โโโ chat/
โ โ โ โโโ chat.routes.js
โ โ โ โโโ chat.controller.js
โ โ โ โโโ chat.service.js
โ โ โ โโโ chat.gateway.js โ socket.io handlers
โ โ โโโ admin/
โ โ โ โโโ admin.routes.js
โ โ โ โโโ admin.controller.js
โ โ โ โโโ admin.service.js
โ โ โโโ landing/
โ โ โโโ landing.routes.js
โ โ โโโ landing.controller.js
โ โ โโโ landing.service.js
โ โ
โ โโโ lib/
โ โ โโโ jwt.js โ sign/verify helpers
โ โ โโโ AppError.js โ custom error class
โ โ โโโ pagination.js โ pagination helper
โ โ
โ โโโ socket/
โ โ โโโ index.js โ socket.io setup
โ โ
โ โโโ app.js โ express app (no listen)
โ โโโ server.js โ http server + socket + listen
โ
โโโ tests/
โ โโโ unit/
โ โ โโโ auth.service.test.js
โ โ โโโ jwt.test.js
โ โ โโโ validation.test.js
โ โโโ integration/
โ โโโ auth.test.js
โ โโโ templates.test.js
โ โโโ orders.test.js
โ โโโ chat.test.js
โ
โโโ .env
โโโ .env.example
โโโ .env.test
โโโ ecosystem.config.js โ PM2 config
โโโ package.json
2. Environment Variables
# apps/server/.env.example
# ===== App =====
NODE_ENV=development
PORT=5000
CLIENT_URL=http://localhost:5173
# CRITICAL: ุบูุฑ ุงูู
ุณุงุฑ ุฏู ูู ุงูู production
ADMIN_ROUTE_HINT=x9k2-manage
# ===== Database =====
# Connection string ู
ุน PgBouncer ูู ุงูู production
DATABASE_URL="postgresql://designflow_user:StrongPassword123!@localhost:5432/designflow_db?schema=public"
# ููู testing: ูุงุนุฏุฉ ู
ููุตูุฉ
DATABASE_URL_TEST="postgresql://designflow_user:StrongPassword123!@localhost:5432/designflow_test?schema=public"
# ===== Redis =====
REDIS_URL="redis://localhost:6379"
REDIS_PASSWORD="" # ูุงุถู ูู devุ ูู production ูุงุฒู
ูููู ู
ูุฌูุฏ
# ===== JWT =====
# CRITICAL: ุงุณุชุฎุฏู
openssl rand -base64 64 ูุชูููุฏ secrets ุญููููุฉ
JWT_ACCESS_SECRET="CHANGE_ME_USE_OPENSSL_RAND_64_CHARS_MINIMUM_HERE"
JWT_REFRESH_SECRET="CHANGE_ME_DIFFERENT_FROM_ACCESS_SECRET_HERE"
JWT_ACCESS_EXPIRES="15m"
JWT_REFRESH_EXPIRES="7d"
# ===== Cloudinary =====
CLOUDINARY_CLOUD_NAME=""
CLOUDINARY_API_KEY=""
CLOUDINARY_API_SECRET=""
# ===== Rate Limiting =====
RATE_LIMIT_WINDOW_MS=900000 # 15 ุฏูููุฉ
RATE_LIMIT_MAX_GENERAL=100
RATE_LIMIT_MAX_AUTH=5
RATE_LIMIT_MAX_REGISTER=3
3. Config Layer
// src/config/index.js
import { z } from 'zod'
import dotenv from 'dotenv'
dotenv.config()
// Validate ENV vars ุนูุฏ ุงูู startup โ ุชูู ุงูุชุทุจูู ูู ููู ุญุงุฌุฉ ูุงูุตุฉ
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().default('5000').transform(Number),
CLIENT_URL: z.string().url(),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string(),
JWT_ACCESS_SECRET: z.string().min(32, 'JWT_ACCESS_SECRET must be at least 32 chars'),
JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 chars'),
JWT_ACCESS_EXPIRES: z.string().default('15m'),
JWT_REFRESH_EXPIRES:z.string().default('7d'),
CLOUDINARY_CLOUD_NAME: z.string(),
CLOUDINARY_API_KEY: z.string(),
CLOUDINARY_API_SECRET: z.string(),
})
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error('โ Invalid environment variables:', parsed.error.flatten().fieldErrors)
process.exit(1)
}
export const config = parsed.data
// src/config/database.js
import { PrismaClient } from '@prisma/client'
// Singleton โ ูุง ุชุนู
ู instance ุฌุฏูุฏุฉ ูู ูู request
const globalForPrisma = globalThis
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// ุงุฎุชุจุงุฑ ุงูุงุชุตุงู
export async function connectDB() {
try {
await prisma.$connect()
console.log('โ
PostgreSQL connected')
} catch (err) {
console.error('โ Database connection failed:', err)
process.exit(1)
}
}
// src/config/redis.js
import Redis from 'ioredis'
import { config } from './index.js'
export const redis = new Redis(config.REDIS_URL, {
password: process.env.REDIS_PASSWORD || undefined,
retryStrategy: (times) => Math.min(times * 100, 3000), // retry ู
ุน backoff
maxRetriesPerRequest: 3,
enableReadyCheck: true,
})
redis.on('connect', () => console.log('โ
Redis connected'))
redis.on('error', (err) => console.error('โ Redis error:', err))
4. Prisma Schema ุงููุงู ู
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ENUMS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
enum Role {
CLIENT
SUPPORT
ADMIN
}
enum OrderStatus {
PENDING
ACCEPTED
IN_PROGRESS
COMPLETED
CANCELLED
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// MODELS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
model User {
id String @id @default(cuid())
firstName String @db.VarChar(50)
lastName String @db.VarChar(50)
phone String @unique @db.VarChar(20)
email String @unique @db.VarChar(255)
passwordHash String @db.Text
avatarUrl String? @db.Text
role Role @default(CLIENT)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
orders Order[]
sentMessages Message[] @relation("SentMessages")
refreshTokens RefreshToken[]
@@index([email])
@@index([phone])
@@index([role])
@@map("users")
}
model RefreshToken {
id String @id @default(cuid())
token String @unique @db.Text // hashed token
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt]) // ูุญุฐู ุงูู
ูุชููุฉ ุชููุงุฆูุงู
@@map("refresh_tokens")
}
model Template {
id String @id @default(cuid())
title String @db.VarChar(100)
description String @db.Text
category String @db.VarChar(50)
price Decimal @db.Decimal(10, 2)
previewUrl String @db.Text
demoUrl String? @db.Text
tags String[]
isPublished Boolean @default(false)
defaultColors Json // { primary, secondary, accent, text }
components Json // { sections: [...], colorBindings: {...} }
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
@@index([isPublished])
@@index([category])
@@index([sortOrder])
@@map("templates")
}
model Order {
id String @id @default(cuid())
orderNumber String @unique
userId String
user User @relation(fields: [userId], references: [id])
templateId String
template Template @relation(fields: [templateId], references: [id])
status OrderStatus @default(PENDING)
customization Json // { sections: [...], colorTokens: {...} }
totalAmount Decimal @db.Decimal(10, 2)
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
conversation Conversation?
@@index([userId])
@@index([status])
@@index([createdAt])
@@map("orders")
}
model Conversation {
id String @id @default(cuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
isOpen Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[]
@@map("conversations")
}
model Message {
id String @id @default(cuid())
conversationId String
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
senderId String
sender User @relation("SentMessages", fields: [senderId], references: [id])
content String @db.Text
isRead Boolean @default(false)
createdAt DateTime @default(now())
@@index([conversationId])
@@index([createdAt])
@@index([isRead])
@@map("messages")
}
model LandingContent {
id String @id @default(cuid())
section String @unique @db.VarChar(50) // "hero", "features", etc.
content Json
updatedAt DateTime @updatedAt
@@map("landing_content")
}
4.1 Migration & Seed
# ุฅูุดุงุก migration
npx prisma migrate dev --name init
# ุชูููุฏ Prisma Client
npx prisma generate
# Seed (ุฅูุดุงุก ุฃุฏู
ู ุฃููู + landing content)
npx prisma db seed
// prisma/seed.js
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
// Admin user
const hashedPass = await bcrypt.hash('Admin@123456', 12)
await prisma.user.upsert({
where: { email: 'admin@designflow.com' },
update: {},
create: {
firstName: 'Admin',
lastName: 'System',
phone: '+201000000000',
email: 'admin@designflow.com',
passwordHash: hashedPass,
role: 'ADMIN',
}
})
// Default landing content
const sections = ['hero', 'features', 'how_it_works', 'testimonials', 'footer']
for (const section of sections) {
await prisma.landingContent.upsert({
where: { section },
update: {},
create: { section, content: getDefaultContent(section) }
})
}
console.log('โ
Seed completed')
}
main().catch(console.error).finally(() => prisma.$disconnect())
5. Security Layer โ ูู ุงูู Middleware
5.1 AppError Class
// src/lib/AppError.js
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message)
this.statusCode = statusCode
this.code = code
this.isOperational = true
Error.captureStackTrace(this, this.constructor)
}
}
// ุงุณุชุฎุฏุงู
ู
ูุญุฏ ูู ูู ู
ูุงู:
// throw new AppError('ุงูู
ุณุชุฎุฏู
ุบูุฑ ู
ูุฌูุฏ', 404, 'USER_NOT_FOUND')
// throw new AppError('ุงูุจูุงูุงุช ุบูุฑ ุตุญูุญุฉ', 400, 'VALIDATION_ERROR')
// throw new AppError('ุบูุฑ ู
ุตุฑุญ', 401, 'UNAUTHORIZED')
// throw new AppError('ู
ู
ููุน ุงููุตูู', 403, 'FORBIDDEN')
5.2 Global Error Handler
// src/middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
// Log non-operational errors (programming bugs)
if (!err.isOperational) {
console.error('๐ฅ CRITICAL ERROR:', err)
// ูู production: ุฃุฑุณู ูู Sentry/DataDog
}
// Prisma errors
if (err.code === 'P2002') {
return res.status(409).json({ error: 'ูุฐุง ุงูุจุฑูุฏ ุฃู ุฑูู
ุงููุงุชู ู
ุณุชุฎุฏู
ุจุงููุนู', code: 'DUPLICATE_ENTRY' })
}
if (err.code === 'P2025') {
return res.status(404).json({ error: 'ุงูุนูุตุฑ ุบูุฑ ู
ูุฌูุฏ', code: 'NOT_FOUND' })
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'ุฑู
ุฒ ุงูู
ุตุงุฏูุฉ ุบูุฑ ุตุญูุญ', code: 'INVALID_TOKEN' })
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'ุงูุชูุช ุตูุงุญูุฉ ุงูุฌูุณุฉ', code: 'TOKEN_EXPIRED' })
}
// Zod validation errors
if (err.name === 'ZodError') {
return res.status(400).json({ error: 'ุจูุงูุงุช ุบูุฑ ุตุญูุญุฉ', code: 'VALIDATION_ERROR', details: err.flatten() })
}
// Operational errors (expected)
if (err.isOperational) {
return res.status(err.statusCode).json({ error: err.message, code: err.code })
}
// Unknown errors โ ูุง ุชูุดู ุงูุชูุงุตูู
return res.status(500).json({ error: 'ุญุฏุซ ุฎุทุฃ ุฏุงุฎูู', code: 'INTERNAL_ERROR' })
}
5.3 Auth Middleware
// src/middleware/auth.js
import jwt from 'jsonwebtoken'
import { config } from '../config/index.js'
import { redis } from '../config/redis.js'
import { AppError } from '../lib/AppError.js'
import { prisma } from '../config/database.js'
export async function authenticate(req, res, next) {
try {
// 1. Extract token
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
throw new AppError('ูู
ูุชู
ุชูููุฑ ุฑู
ุฒ ุงูู
ุตุงุฏูุฉ', 401, 'NO_TOKEN')
}
const token = authHeader.split(' ')[1]
// 2. Check blacklist (Redis) โ ููู tokens ุงูู
ูุจุทูุฉ ุจุนุฏ logout
const isBlacklisted = await redis.get(`blacklist:${token}`)
if (isBlacklisted) throw new AppError('ุฑู
ุฒ ุงูู
ุตุงุฏูุฉ ู
ูุบู', 401, 'TOKEN_REVOKED')
// 3. Verify JWT
const decoded = jwt.verify(token, config.JWT_ACCESS_SECRET)
// 4. Check user still exists and is active
const user = await prisma.user.findUnique({
where: { id: decoded.sub },
select: { id: true, role: true, isActive: true, email: true, firstName: true, lastName: true }
})
if (!user || !user.isActive) {
throw new AppError('ุงูู
ุณุชุฎุฏู
ุบูุฑ ู
ูุฌูุฏ ุฃู ู
ุญุธูุฑ', 401, 'USER_NOT_FOUND')
}
req.user = user
next()
} catch (err) {
next(err)
}
}
// Role-based access
export function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) return next(new AppError('ุบูุฑ ู
ุตุงุฏู', 401, 'UNAUTHORIZED'))
if (!roles.includes(req.user.role)) {
return next(new AppError('ููุณ ูุฏูู ุตูุงุญูุฉ', 403, 'FORBIDDEN'))
}
next()
}
}
// Shortcuts
export const requireAdmin = requireRole('ADMIN')
export const requireSupport = requireRole('ADMIN', 'SUPPORT')
5.4 Validation Middleware
// src/middleware/validate.js
export function validate(schema, source = 'body') {
return (req, res, next) => {
const data = source === 'body' ? req.body
: source === 'params' ? req.params
: req.query
const result = schema.safeParse(data)
if (!result.success) {
return res.status(400).json({
error: 'ุจูุงูุงุช ุบูุฑ ุตุญูุญุฉ',
code: 'VALIDATION_ERROR',
details: result.error.flatten().fieldErrors
})
}
req.validatedData = result.data
next()
}
}
5.5 Rate Limiters
// src/middleware/rateLimiter.js
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
import { redis } from '../config/redis.js'
const makeStore = (prefix) => new RedisStore({
prefix: `rl:${prefix}:`,
sendCommand: (...args) => redis.call(...args),
})
// General API rate limit
export const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: makeStore('general'),
standardHeaders: true,
legacyHeaders: false,
message: { error: 'ุนุฏุฏ ูุจูุฑ ุฌุฏุงู ู
ู ุงูุทูุจุงุชุ ุญุงูู ุจุนุฏ 15 ุฏูููุฉ', code: 'RATE_LIMIT_EXCEEDED' }
})
// Auth endpoints โ ุฃุดุฏ ุชูููุฏุงู
export const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 ู
ุญุงููุงุช login ููุท
store: makeStore('login'),
skipSuccessfulRequests: true, // ูุง ูุญุณุจ ุงูู successful logins
message: { error: 'ุชุฌุงูุฒุช ุงูุญุฏ ุงูู
ุณู
ูุญุ ุญุงูู ุจุนุฏ 15 ุฏูููุฉ', code: 'TOO_MANY_LOGIN_ATTEMPTS' }
})
export const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // ุณุงุนุฉ
max: 3,
store: makeStore('register'),
message: { error: 'ุญุฏ ุงูุชุณุฌููุ ุญุงูู ุจุนุฏ ุณุงุนุฉ', code: 'TOO_MANY_REGISTRATIONS' }
})
// Upload limiter
export const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 20,
store: makeStore('upload'),
message: { error: 'ุญุฏ ุงูุฑูุนุ ุญุงูู ุจุนุฏ ุณุงุนุฉ', code: 'UPLOAD_RATE_LIMIT' }
})
5.6 Upload Middleware
// src/middleware/upload.js
import multer from 'multer'
import { v2 as cloudinary } from 'cloudinary'
import { CloudinaryStorage } from 'multer-storage-cloudinary'
import { config } from '../config/index.js'
import { AppError } from '../lib/AppError.js'
cloudinary.config({
cloud_name: config.CLOUDINARY_CLOUD_NAME,
api_key: config.CLOUDINARY_API_KEY,
api_secret: config.CLOUDINARY_API_SECRET,
})
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp']
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
function makeCloudinaryStorage(folder) {
return new CloudinaryStorage({
cloudinary,
params: { folder: `designflow/${folder}`, allowed_formats: ['jpg', 'jpeg', 'png', 'webp'] }
})
}
function fileFilter(req, file, cb) {
if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new AppError('ููุน ุงูู
ูู ุบูุฑ ู
ุณู
ูุญ (ููุท JPEG, PNG, WebP)', 400, 'INVALID_FILE_TYPE'))
}
}
export const uploadAvatar = multer({
storage: makeCloudinaryStorage('avatars'),
limits: { fileSize: MAX_FILE_SIZE },
fileFilter,
}).single('avatar')
export const uploadTemplateImage = multer({
storage: makeCloudinaryStorage('templates'),
limits: { fileSize: MAX_FILE_SIZE * 2 }, // 10MB ููููุงูุจ
fileFilter,
}).single('preview')
6. JWT Service
// src/lib/jwt.js
import jwt from 'jsonwebtoken'
import { config } from '../config/index.js'
import { redis } from '../config/redis.js'
export function signAccessToken(userId, role) {
return jwt.sign(
{ sub: userId, role, type: 'access' },
config.JWT_ACCESS_SECRET,
{ expiresIn: config.JWT_ACCESS_EXPIRES }
)
}
export function signRefreshToken(userId) {
return jwt.sign(
{ sub: userId, type: 'refresh' },
config.JWT_REFRESH_SECRET,
{ expiresIn: config.JWT_REFRESH_EXPIRES }
)
}
export function verifyAccessToken(token) {
return jwt.verify(token, config.JWT_ACCESS_SECRET)
}
export function verifyRefreshToken(token) {
return jwt.verify(token, config.JWT_REFRESH_SECRET)
}
// Blacklist access token ุนูุฏ ุงูู logout
export async function blacklistToken(token) {
const decoded = jwt.decode(token)
if (!decoded?.exp) return
const ttl = decoded.exp - Math.floor(Date.now() / 1000)
if (ttl > 0) await redis.setex(`blacklist:${token}`, ttl, '1')
}
// Socket.IO auth
export async function verifySocketToken(token) {
try {
const decoded = verifyAccessToken(token)
const isBlacklisted = await redis.get(`blacklist:${token}`)
if (isBlacklisted) return null
return decoded
} catch {
return null
}
}
7. Auth Module ุงููุงู ู
7.1 Schemas
// src/modules/auth/auth.schema.js
import { z } from 'zod'
export const registerSchema = z.object({
firstName: z.string().min(2).max(50).regex(/^[\p{L}\s]+$/u, 'ุงุณู
ุบูุฑ ุตุญูุญ'),
lastName: z.string().min(2).max(50).regex(/^[\p{L}\s]+$/u, 'ุงุณู
ุบูุฑ ุตุญูุญ'),
phone: z.string().regex(/^\+?[0-9]{10,15}$/, 'ุฑูู
ูุงุชู ุบูุฑ ุตุญูุญ'),
email: z.string().email('ุจุฑูุฏ ุฅููุชุฑููู ุบูุฑ ุตุญูุญ').toLowerCase(),
password: z.string()
.min(8, 'ููู
ุฉ ุงูู
ุฑูุฑ 8 ุฃุญุฑู ุนูู ุงูุฃูู')
.regex(/^(?=.*[A-Z])/, 'ูุฌุจ ุฃู ุชุญุชูู ุนูู ุญุฑู ูุจูุฑ')
.regex(/^(?=.*[0-9])/, 'ูุฌุจ ุฃู ุชุญุชูู ุนูู ุฑูู
')
.regex(/^(?=.*[^A-Za-z0-9])/, 'ูุฌุจ ุฃู ุชุญุชูู ุนูู ุฑู
ุฒ ุฎุงุต'),
confirmPassword: z.string()
}).refine(d => d.password === d.confirmPassword, {
message: 'ููู
ุชุง ุงูู
ุฑูุฑ ุบูุฑ ู
ุชุทุงุจูุชุงู',
path: ['confirmPassword']
})
export const loginSchema = z.object({
email: z.string().email().toLowerCase(),
password: z.string().min(1)
})
export const changePasswordSchema = z.object({
currentPassword: z.string().min(1),
newPassword: z.string().min(8)
.regex(/^(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).+$/)
})
7.2 Service
// src/modules/auth/auth.service.js
import bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { prisma } from '../../config/database.js'
import { redis } from '../../config/redis.js'
import { signAccessToken, signRefreshToken, verifyRefreshToken, blacklistToken } from '../../lib/jwt.js'
import { AppError } from '../../lib/AppError.js'
const BCRYPT_ROUNDS = 12
const REFRESH_TOKEN_EXPIRES_DAYS = 7
export async function registerUser(data, avatarUrl = null) {
// Check duplicates
const existing = await prisma.user.findFirst({
where: { OR: [{ email: data.email }, { phone: data.phone }] }
})
if (existing) {
const field = existing.email === data.email ? 'ุงูุจุฑูุฏ ุงูุฅููุชุฑููู' : 'ุฑูู
ุงููุงุชู'
throw new AppError(`${field} ู
ุณุชุฎุฏู
ุจุงููุนู`, 409, 'DUPLICATE_ENTRY')
}
const passwordHash = await bcrypt.hash(data.password, BCRYPT_ROUNDS)
const user = await prisma.user.create({
data: {
firstName: data.firstName,
lastName: data.lastName,
phone: data.phone,
email: data.email,
passwordHash,
avatarUrl,
role: 'CLIENT'
},
select: { id: true, firstName: true, lastName: true, email: true, role: true, avatarUrl: true }
})
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
return { user, accessToken, refreshToken }
}
export async function loginUser(email, password) {
const user = await prisma.user.findUnique({
where: { email },
select: { id: true, firstName: true, lastName: true, email: true, role: true, avatarUrl: true, passwordHash: true, isActive: true }
})
// SECURITY: ููุณ ุงูู error message ููู user ุบูุฑ ุงูู
ูุฌูุฏ ูุงูู password ุงูุบูุท
// ู
ูุน User Enumeration attacks
const isValid = user ? await bcrypt.compare(password, user.passwordHash) : await bcrypt.hash(password, 1)
if (!user || !isValid) throw new AppError('ุจูุงูุงุช ุชุณุฌูู ุงูุฏุฎูู ุบูุฑ ุตุญูุญุฉ', 401, 'INVALID_CREDENTIALS')
if (!user.isActive) throw new AppError('ุงูุญุณุงุจ ู
ููููุ ุชูุงุตู ู
ุน ุงูุฏุนู
', 403, 'ACCOUNT_SUSPENDED')
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
const { passwordHash, ...safeUser } = user
return { user: safeUser, accessToken, refreshToken }
}
export async function refreshUserToken(oldRefreshToken) {
if (!oldRefreshToken) throw new AppError('ูู
ูุชู
ุชูููุฑ refresh token', 401, 'NO_REFRESH_TOKEN')
// Verify the JWT
let decoded
try { decoded = verifyRefreshToken(oldRefreshToken) }
catch { throw new AppError('Refresh token ู
ูุชูู ุฃู ุบูุฑ ุตุญูุญ', 401, 'INVALID_REFRESH_TOKEN') }
// Check exists in DB (rotation: ูู refresh โ delete old, create new)
const hashedOld = await hashToken(oldRefreshToken)
const stored = await prisma.refreshToken.findUnique({ where: { token: hashedOld } })
if (!stored || stored.expiresAt < new Date()) {
// Possible token reuse attack โ revoke all user tokens
await prisma.refreshToken.deleteMany({ where: { userId: decoded.sub } })
throw new AppError('Refresh token ุบูุฑ ุตุญูุญ', 401, 'REFRESH_TOKEN_REUSE')
}
// Delete old, create new (rotation)
await prisma.refreshToken.delete({ where: { token: hashedOld } })
const user = await prisma.user.findUnique({
where: { id: decoded.sub },
select: { id: true, firstName: true, lastName: true, email: true, role: true, avatarUrl: true, isActive: true }
})
if (!user || !user.isActive) throw new AppError('ุงูู
ุณุชุฎุฏู
ุบูุฑ ู
ูุฌูุฏ', 401, 'USER_NOT_FOUND')
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
return { user, accessToken, refreshToken }
}
export async function logoutUser(userId, accessToken) {
// Blacklist current access token
if (accessToken) await blacklistToken(accessToken)
// Delete all refresh tokens for this user
await prisma.refreshToken.deleteMany({ where: { userId } })
}
// โโโ Helpers โโโ
async function createTokenPair(userId, role) {
const accessToken = signAccessToken(userId, role)
const refreshToken = signRefreshToken(userId)
const hashedRefresh = await hashToken(refreshToken)
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_DAYS * 24 * 60 * 60 * 1000)
await prisma.refreshToken.create({
data: { token: hashedRefresh, userId, expiresAt }
})
return { accessToken, refreshToken }
}
async function hashToken(token) {
// Hash the refresh token before storing (bcrypt ู
ุด ุถุฑูุฑู ููุงุ sha256 ูุงูู)
const { createHash } = await import('crypto')
return createHash('sha256').update(token).digest('hex')
}
7.3 Controller
// src/modules/auth/auth.controller.js
import * as authService from './auth.service.js'
import { uploadAvatar } from '../../middleware/upload.js'
const REFRESH_COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/auth/refresh' // Scope ู
ุญุฏูุฏ ูููููู
}
export async function register(req, res, next) {
try {
const avatarUrl = req.file?.path || null
const { user, accessToken, refreshToken } = await authService.registerUser(req.validatedData, avatarUrl)
res.cookie('refresh_token', refreshToken, REFRESH_COOKIE_OPTIONS)
res.status(201).json({ user, accessToken })
} catch (err) { next(err) }
}
export async function login(req, res, next) {
try {
const { email, password } = req.validatedData
const { user, accessToken, refreshToken } = await authService.loginUser(email, password)
res.cookie('refresh_token', refreshToken, REFRESH_COOKIE_OPTIONS)
res.json({ user, accessToken })
} catch (err) { next(err) }
}
export async function refresh(req, res, next) {
try {
const oldToken = req.cookies.refresh_token
const { user, accessToken, refreshToken } = await authService.refreshUserToken(oldToken)
res.cookie('refresh_token', refreshToken, REFRESH_COOKIE_OPTIONS)
res.json({ user, accessToken })
} catch (err) { next(err) }
}
export async function logout(req, res, next) {
try {
const accessToken = req.headers.authorization?.split(' ')[1]
await authService.logoutUser(req.user.id, accessToken)
res.clearCookie('refresh_token', { path: '/api/auth/refresh' })
res.json({ message: 'ุชู
ุชุณุฌูู ุงูุฎุฑูุฌ ุจูุฌุงุญ' })
} catch (err) { next(err) }
}
export async function getMe(req, res) {
res.json(req.user)
}
7.4 Routes
// src/modules/auth/auth.routes.js
import { Router } from 'express'
import { uploadAvatar } from '../../middleware/upload.js'
import { validate } from '../../middleware/validate.js'
import { authenticate } from '../../middleware/auth.js'
import { loginLimiter, registerLimiter } from '../../middleware/rateLimiter.js'
import { registerSchema, loginSchema } from './auth.schema.js'
import * as ctrl from './auth.controller.js'
const router = Router()
router.post('/register', registerLimiter, uploadAvatar, validate(registerSchema), ctrl.register)
router.post('/login', loginLimiter, validate(loginSchema), ctrl.login)
router.post('/refresh', ctrl.refresh)
router.post('/logout', authenticate, ctrl.logout)
router.get('/me', authenticate, ctrl.getMe)
export default router
8. Templates Module
8.1 Service (ู ุน Redis Caching)
// src/modules/templates/templates.service.js
import { prisma } from '../../config/database.js'
import { redis } from '../../config/redis.js'
import { AppError } from '../../lib/AppError.js'
const CACHE_TTL = 300 // 5 ุฏูุงุฆู
const CACHE_PREFIX = 'templates:'
export async function getPublishedTemplates({ category, search, page = 1, limit = 12 }) {
const cacheKey = `${CACHE_PREFIX}list:${JSON.stringify({ category, search, page, limit })}`
// Try cache first
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
const where = {
isPublished: true,
...(category && { category }),
...(search && {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
]
})
}
const [templates, total] = await Promise.all([
prisma.template.findMany({
where,
select: {
id: true, title: true, description: true, category: true,
price: true, previewUrl: true, tags: true, defaultColors: true
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
take: limit,
skip: (page - 1) * limit,
}),
prisma.template.count({ where })
])
const result = { templates, total, page, totalPages: Math.ceil(total / limit) }
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(result))
return result
}
export async function getTemplateById(id) {
const cacheKey = `${CACHE_PREFIX}${id}`
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
const template = await prisma.template.findUnique({
where: { id, isPublished: true },
select: {
id: true, title: true, description: true, category: true,
price: true, previewUrl: true, demoUrl: true, tags: true,
defaultColors: true, components: true
}
})
if (!template) throw new AppError('ุงููุงูุจ ุบูุฑ ู
ูุฌูุฏ', 404, 'TEMPLATE_NOT_FOUND')
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(template))
return template
}
// Invalidate cache ุนูุฏ ุฃู ุชุนุฏูู
export async function invalidateTemplateCache(id = null) {
if (id) await redis.del(`${CACHE_PREFIX}${id}`)
// Delete all list caches (pattern delete)
const keys = await redis.keys(`${CACHE_PREFIX}list:*`)
if (keys.length) await redis.del(...keys)
}
9. Orders Module
// src/modules/orders/orders.service.js
import { prisma } from '../../config/database.js'
import { AppError } from '../../lib/AppError.js'
import { createConversationWithWelcome } from '../chat/chat.service.js'
import { v4 as uuidv4 } from 'uuid'
export async function createOrder(userId, { templateId, customization, notes }) {
// Verify template exists
const template = await prisma.template.findUnique({
where: { id: templateId, isPublished: true },
select: { id: true, price: true, title: true }
})
if (!template) throw new AppError('ุงููุงูุจ ุบูุฑ ู
ูุฌูุฏ', 404, 'TEMPLATE_NOT_FOUND')
const order = await prisma.order.create({
data: {
orderNumber: `ORD-${Date.now()}-${uuidv4().slice(0, 6).toUpperCase()}`,
userId,
templateId,
customization,
totalAmount: template.price,
notes,
status: 'PENDING'
},
include: { template: { select: { title: true, previewUrl: true } } }
})
return order
}
export async function getUserOrders(userId, { page = 1, limit = 10, status }) {
const where = { userId, ...(status && { status }) }
const [orders, total] = await Promise.all([
prisma.order.findMany({
where,
include: {
template: { select: { title: true, previewUrl: true, category: true } },
conversation: { select: { id: true, isOpen: true } }
},
orderBy: { createdAt: 'desc' },
take: limit,
skip: (page - 1) * limit,
}),
prisma.order.count({ where })
])
return { orders, total, totalPages: Math.ceil(total / limit) }
}
// Admin: ุชุบููุฑ ุญุงูุฉ ุงูุทูุจ
export async function updateOrderStatus(orderId, newStatus, adminId) {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { conversation: true }
})
if (!order) throw new AppError('ุงูุทูุจ ุบูุฑ ู
ูุฌูุฏ', 404, 'ORDER_NOT_FOUND')
const updated = await prisma.order.update({
where: { id: orderId },
data: { status: newStatus }
})
// ูู
ุง ุงูุทูุจ ูุชูุจู โ ุฑุณุงูุฉ ุชููุงุฆูุฉ ููุนู
ูู
if (newStatus === 'ACCEPTED') {
await createConversationWithWelcome(orderId, order.userId, adminId)
}
return updated
}
10. Chat Module + Socket.IO
10.1 Chat Service
// src/modules/chat/chat.service.js
import { prisma } from '../../config/database.js'
import { AppError } from '../../lib/AppError.js'
export async function createConversationWithWelcome(orderId, clientId, adminId) {
// ูู ู
ูุฌูุฏุฉ ุจุงููุนู โ return existing
const existing = await prisma.conversation.findUnique({ where: { orderId } })
if (existing) return existing
const conversation = await prisma.conversation.create({
data: {
orderId,
isOpen: true,
messages: {
create: {
senderId: adminId,
content: 'ู
ุฑุญุจุงู! ุชู
ูุจูู ุทูุจู ูุณูุชูุงุตู ู
ุนู ูุฑูููุง ูุฑูุจุงู ูู
ูุงูุดุฉ ุงูุชูุงุตูู. ๐',
}
}
},
include: { messages: true }
})
return conversation
}
export async function getConversationMessages(conversationId, userId, userRole, { cursor, limit = 20 }) {
// Verify access
const conv = await prisma.conversation.findUnique({
where: { id: conversationId },
include: { order: { select: { userId: true } } }
})
if (!conv) throw new AppError('ุงูู
ุญุงุฏุซุฉ ุบูุฑ ู
ูุฌูุฏุฉ', 404, 'CONVERSATION_NOT_FOUND')
const isAdmin = ['ADMIN', 'SUPPORT'].includes(userRole)
if (!isAdmin && conv.order.userId !== userId) {
throw new AppError('ููุณ ูุฏูู ุตูุงุญูุฉ ุงููุตูู ููุฐู ุงูู
ุญุงุฏุซุฉ', 403, 'FORBIDDEN')
}
const messages = await prisma.message.findMany({
where: { conversationId },
include: { sender: { select: { firstName: true, lastName: true, avatarUrl: true, role: true } } },
orderBy: { createdAt: 'asc' },
take: limit,
...(cursor && { cursor: { id: cursor }, skip: 1 })
})
return messages
}
export async function saveMessage(conversationId, senderId, content) {
// Verify conversation is open
const conv = await prisma.conversation.findUnique({ where: { id: conversationId } })
if (!conv?.isOpen) throw new AppError('ุงูู
ุญุงุฏุซุฉ ู
ุบููุฉ', 403, 'CONVERSATION_CLOSED')
const message = await prisma.message.create({
data: { conversationId, senderId, content },
include: { sender: { select: { firstName: true, lastName: true, avatarUrl: true, role: true } } }
})
return message
}
10.2 Socket.IO Gateway
// src/modules/chat/chat.gateway.js
import { verifySocketToken } from '../../lib/jwt.js'
import { prisma } from '../../config/database.js'
import { saveMessage } from './chat.service.js'
export function setupChatGateway(io) {
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
const decoded = await verifySocketToken(token)
if (!decoded) return next(new Error('Unauthorized'))
// Attach user to socket
const user = await prisma.user.findUnique({
where: { id: decoded.sub },
select: { id: true, firstName: true, lastName: true, role: true, avatarUrl: true }
})
if (!user) return next(new Error('User not found'))
socket.user = user
next()
})
io.on('connection', (socket) => {
console.log(`๐ User connected: ${socket.user.id}`)
// Join personal room
socket.join(`user:${socket.user.id}`)
socket.on('join_conversation', async (conversationId) => {
// Verify access before joining room
try {
const conv = await prisma.conversation.findUnique({
where: { id: conversationId },
include: { order: { select: { userId: true } } }
})
if (!conv) return
const isAdmin = ['ADMIN', 'SUPPORT'].includes(socket.user.role)
const isOwner = conv.order.userId === socket.user.id
if (isAdmin || isOwner) {
socket.join(`conv:${conversationId}`)
socket.emit('joined_conversation', { conversationId })
}
} catch (err) {
socket.emit('error', { message: 'ุฎุทุฃ ูู ุงูุงูุถู
ุงู
ููู
ุญุงุฏุซุฉ' })
}
})
socket.on('send_message', async ({ conversationId, content }) => {
try {
// Input validation
if (!content?.trim() || content.length > 2000) {
return socket.emit('error', { message: 'ุฑุณุงูุฉ ุบูุฑ ุตุญูุญุฉ' })
}
const message = await saveMessage(conversationId, socket.user.id, content.trim())
// Emit to all in the conversation room
io.to(`conv:${conversationId}`).emit('new_message', message)
// Notify the other party (push notification style)
const conv = await prisma.conversation.findUnique({
where: { id: conversationId },
include: { order: { select: { userId: true } } }
})
const targetUserId = socket.user.role === 'CLIENT' ? null : conv?.order.userId
if (targetUserId) {
io.to(`user:${targetUserId}`).emit('message_notification', {
conversationId,
senderName: `${socket.user.firstName} ${socket.user.lastName}`,
preview: content.slice(0, 50)
})
}
} catch (err) {
socket.emit('error', { message: err.message })
}
})
socket.on('mark_read', async ({ conversationId }) => {
await prisma.message.updateMany({
where: { conversationId, isRead: false, senderId: { not: socket.user.id } },
data: { isRead: true }
})
io.to(`conv:${conversationId}`).emit('messages_read', { conversationId, readBy: socket.user.id })
})
socket.on('disconnect', () => {
console.log(`๐ User disconnected: ${socket.user.id}`)
})
})
}
11. App.js + Server.js
// src/app.js
import express from 'express'
import helmet from 'helmet'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import compression from 'compression'
import morgan from 'morgan'
import { config } from './config/index.js'
import { generalLimiter } from './middleware/rateLimiter.js'
import { errorHandler } from './middleware/errorHandler.js'
// Routes
import authRoutes from './modules/auth/auth.routes.js'
import usersRoutes from './modules/users/users.routes.js'
import templatesRoutes from './modules/templates/templates.routes.js'
import ordersRoutes from './modules/orders/orders.routes.js'
import chatRoutes from './modules/chat/chat.routes.js'
import adminRoutes from './modules/admin/admin.routes.js'
import landingRoutes from './modules/landing/landing.routes.js'
const app = express()
// โโโ Security Headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(helmet({
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'res.cloudinary.com'],
connectSrc: ["'self'", config.CLIENT_URL],
}
}
}))
// โโโ CORS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(cors({
origin: (origin, callback) => {
const allowed = [config.CLIENT_URL, 'http://localhost:5173']
if (!origin || allowed.includes(origin)) callback(null, true)
else callback(new Error('Not allowed by CORS'))
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}))
// โโโ Body Parsing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(express.json({ limit: '10kb' })) // ู
ูุน large JSON payloads
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(compression())
// โโโ Logging โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (config.NODE_ENV !== 'test') {
app.use(morgan(config.NODE_ENV === 'development' ? 'dev' : 'combined'))
}
// โโโ Rate Limiting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('/api', generalLimiter)
// โโโ Health Check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }))
// โโโ Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('/api/auth', authRoutes)
app.use('/api/users', usersRoutes)
app.use('/api/templates', templatesRoutes)
app.use('/api/orders', ordersRoutes)
app.use('/api/chat', chatRoutes)
app.use('/api/admin', adminRoutes)
app.use('/api/landing', landingRoutes)
// โโโ 404 Handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('*', (req, res) => {
res.status(404).json({ error: 'ุงูู
ุณุงุฑ ุบูุฑ ู
ูุฌูุฏ', code: 'ROUTE_NOT_FOUND' })
})
// โโโ Global Error Handler (MUST be last) โโโโโโโโโโโโโโโโโ
app.use(errorHandler)
export default app
// src/server.js
import { createServer } from 'http'
import { Server as SocketIOServer } from 'socket.io'
import app from './app.js'
import { connectDB } from './config/database.js'
import { redis } from './config/redis.js'
import { setupChatGateway } from './modules/chat/chat.gateway.js'
import { config } from './config/index.js'
async function bootstrap() {
// Connect DB
await connectDB()
// Create HTTP server
const httpServer = createServer(app)
// Setup Socket.IO
const io = new SocketIOServer(httpServer, {
cors: { origin: config.CLIENT_URL, credentials: true },
transports: ['websocket', 'polling'],
})
setupChatGateway(io)
// Start server
httpServer.listen(config.PORT, () => {
console.log(`๐ Server running on port ${config.PORT}`)
console.log(`๐ Environment: ${config.NODE_ENV}`)
})
// Graceful shutdown
const shutdown = async (signal) => {
console.log(`\n${signal} received. Shutting down gracefully...`)
httpServer.close(async () => {
await prisma.$disconnect()
await redis.quit()
console.log('โ
Server closed.')
process.exit(0)
})
setTimeout(() => process.exit(1), 10000) // Force exit after 10s
}
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
// Unhandled rejections
process.on('unhandledRejection', (reason) => {
console.error('๐ฅ Unhandled Rejection:', reason)
shutdown('UNHANDLED_REJECTION')
})
}
bootstrap()
12. Admin Routes ุงููุงู ูุฉ
// src/modules/admin/admin.routes.js
import { Router } from 'express'
import { authenticate, requireSupport, requireAdmin } from '../../middleware/auth.js'
import * as ctrl from './admin.controller.js'
const router = Router()
// ูู ุงูู admin routes ู
ุญู
ูุฉ ุจู authenticate + requireSupport/requireAdmin
router.use(authenticate)
// Overview
router.get('/stats', requireAdmin, ctrl.getStats)
// Orders
router.get('/orders', requireSupport, ctrl.getAllOrders)
router.get('/orders/:id', requireSupport, ctrl.getOrderById)
router.patch('/orders/:id/status', requireSupport, ctrl.updateOrderStatus)
router.post('/orders/:id/conversation', requireSupport, ctrl.openConversation)
// Templates (Admin only โ ู
ุด Support)
router.get('/templates', requireAdmin, ctrl.getAllTemplates)
router.post('/templates', requireAdmin, uploadTemplateImage, ctrl.createTemplate)
router.put('/templates/:id', requireAdmin, ctrl.updateTemplate)
router.delete('/templates/:id', requireAdmin, ctrl.deleteTemplate)
router.patch('/templates/:id/publish', requireAdmin, ctrl.togglePublish)
// Landing Content
router.get('/landing', requireAdmin, ctrl.getLandingContent)
router.put('/landing/:section', requireAdmin, ctrl.updateLandingSection)
// Users
router.get('/users', requireAdmin, ctrl.getUsers)
router.get('/users/:id', requireAdmin, ctrl.getUserById)
// Conversations
router.get('/conversations', requireSupport, ctrl.getConversations)
export default router
13. PostgreSQL Performance
13.1 Connection Pooling
// DATABASE_URL ู
ุน PgBouncer ูู ุงูู production:
// postgresql://user:pass@pgbouncer:6432/designflow?pgbouncer=true&connection_limit=5
// ุฃู ุจุงุณุชุฎุฏุงู
Prisma's built-in pooling:
// DATABASE_URL="postgresql://...?connection_limit=10&pool_timeout=20&connect_timeout=10"
13.2 Indexes Strategy
-- ุงูู indexes ุงูู
ูุฌูุฏุฉ ูู ุงูู schema ูุงููุฉ ููู queries ุงูุนุงุฏูุฉ
-- ููู ูู ุงูู production ุฑุงุฌุน slow query log ูุฃุถู indexes ุญุณุจ ุงูุญุงุฌุฉ
-- ู
ุซุงู: ูู ุจุชูquery orders ุจู userId + status ูุชูุฑ:
-- ูู ู
ูุฌูุฏ ุจุงููุนู ูู ุงูู schema ูู @@index([userId]) ู @@index([status])
-- ูู full-text search ุนูู ุงูู templates:
CREATE INDEX idx_templates_search ON templates USING gin(to_tsvector('english', title || ' ' || description));
-- EXPLAIN ANALYZE ููู query ุชุงูู ูู ุงูู production
13.3 Cron: Cleanup Expired Tokens
// src/jobs/cleanup.js โ ุจูุดุชุบู ููู
ูุงู
import { prisma } from '../config/database.js'
export async function cleanupExpiredTokens() {
const deleted = await prisma.refreshToken.deleteMany({
where: { expiresAt: { lt: new Date() } }
})
console.log(`๐งน Deleted ${deleted.count} expired refresh tokens`)
}
// ูู server.jsุ ุงุดุชุบู ูู 24 ุณุงุนุฉ:
// setInterval(cleanupExpiredTokens, 24 * 60 * 60 * 1000)
14. PM2 Production Config
// ecosystem.config.js
module.exports = {
apps: [{
name: 'designflow-api',
script: 'src/server.js',
instances: 'max', // ุงุณุชุฎุฏู
ูู ุงูู CPU cores
exec_mode: 'cluster',
max_memory_restart: '512M',
error_file: 'logs/error.log',
out_file: 'logs/out.log',
merge_logs: true,
env_production: {
NODE_ENV: 'production',
PORT: 5000,
},
// Auto restart on crash
autorestart: true,
watch: false,
restart_delay: 5000,
}]
}
15. Nginx Config (Reverse Proxy)
# /etc/nginx/sites-available/designflow
upstream api_backend {
least_conn; # Load balance ุจูู PM2 instances
server 127.0.0.1:5000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.designflow.com;
ssl_certificate /etc/ssl/certs/designflow.crt;
ssl_certificate_key /etc/ssl/private/designflow.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers (backup ูู helmet)
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Rate limiting ุนูู ู
ุณุชูู Nginx (ูุจู ู
ุง ููุตู Express)
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req zone=api burst=20 nodelay;
location /api {
proxy_pass http://api_backend;
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_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
# Socket.IO
location /socket.io {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Static files (Frontend build)
location / {
root /var/www/designflow/client/dist;
try_files $uri $uri/ /index.html;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# HTTP โ HTTPS redirect
server {
listen 80;
server_name designflow.com api.designflow.com;
return 301 https://$host$request_uri;
}
16. Security Audit Checklist
Authentication
- Passwords hashed ุจู bcrypt (rounds=12)
- JWT secrets ุนุดูุงุฆูุฉ ูุทูููุฉ (64+ chars)
- Access tokens ูุตูุฑุฉ ุงูุนู ุฑ (15 ุฏูููุฉ)
- Refresh tokens ูู httpOnly cookies ููุท
- Refresh token rotation ุจุนุฏ ูู ุงุณุชุฎุฏุงู
- Token blacklisting ุนูุฏ ุงูู logout
- User enumeration protection (ููุณ ุงูู error ูู user ุบูุฑ ู ูุฌูุฏ + password ุบูุท)
- Account lockout ุนูุฏ ู ุญุงููุงุช ูุซูุฑุฉ
Input Validation
- Zod validation ุนูู ูู request body
- File type validation (MIME check + extension)
- File size limits
- JSON size limit (10kb)
- SQL Injection: Prisma parameterized queries
- XSS Prevention: helmet + content validation
Authorization
- ูู route ู ุญู ู ุจู authenticate middleware
- Role-based access control (CLIENT / SUPPORT / ADMIN)
- Resource ownership checks (ุงูุนู ูู ูุดูู ุทูุจุงุชู ุจุณ)
- Admin routes ู ุฎููุฉ (404 ููุบูุฑ ู ุตุฑุญ)
Infrastructure
- HTTPS ูู ุงูู production
- Rate limiting ุนูู ูู endpoint ุญุณุงุณ
- CORS ู ุญุฏูุฏ ุจู allowed origins ููุท
- Helmet headers
- Compression
- Graceful shutdown
- Error messages ูุง ุชูุดู stack traces ูู ุงูู production
- Logs ูุง ุชุญุชูู ุนูู passwords ุฃู tokens
17. Checklist ูุจู ุฅููุงุก ุงูู ุฑุญูุฉ
- ูู ุงูู endpoints ุดุบุงูุฉ ุจู Postman/Thunder Client
- Auth flow ูุงู ู (register โ login โ refresh โ logout)
- Template CRUD + cache
- Order creation + status update + auto-conversation
- Socket.IO chat (ู ู admin โ client ูุนูุณู)
- Rate limiting ุดุบุงู (ุงุฎุชุจุฑ ุจู 10+ requests ู ุชุชุงูููู)
- JWT blacklisting ุจุนุฏ logout
- File upload ูุดุชุบู ุนูู Cloudinary
- Error handler ุจูู ุณู ูู ุงูุฃุฎุทุงุก
- Prisma migrations applied
- PM2 cluster mode ุดุบุงู
- /health endpoint ุจูุฑุฌุน 200
ุงูู ุฑุญูุฉ ุงูุซุงููุฉ ู ูุชููุฉ โ ุงูุชูู ูู PHASE_3_TESTING.md