@@ -0,0 +1,562 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const baseDir = './backend';
|
||||
|
||||
// Create directories
|
||||
const dirs = [
|
||||
`${baseDir}/src/routes`,
|
||||
];
|
||||
|
||||
dirs.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
// package.json
|
||||
fs.writeFileSync(`${baseDir}/package.json`, JSON.stringify({
|
||||
"name": "photobooth-backend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js"
|
||||
},
|
||||
"keywords": ["photobooth", "express", "mysql"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"mysql2": "^3.6.5",
|
||||
"jsonwebtoken": "^9.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}, null, 2));
|
||||
console.log('Created package.json');
|
||||
|
||||
// .env.example
|
||||
fs.writeFileSync(`${baseDir}/.env.example`, `# Backend API
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=photobooth
|
||||
DB_USER=photobooth_app
|
||||
DB_PASSWORD=your_app_password_here
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret_here_change_in_production
|
||||
JWT_EXPIRY=15m
|
||||
REFRESH_TOKEN_EXPIRY=7d
|
||||
|
||||
# Frontend
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
BACKEND_URL=http://localhost:3000
|
||||
|
||||
# File storage
|
||||
UPLOADS_DIR=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
`);
|
||||
console.log('Created .env.example');
|
||||
|
||||
// src/db.js
|
||||
fs.writeFileSync(`${baseDir}/src/db.js`, `import mysql from 'mysql2/promise'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
})
|
||||
|
||||
export async function query(sql, values) {
|
||||
const connection = await pool.getConnection()
|
||||
try {
|
||||
const [results] = await connection.execute(sql, values)
|
||||
return results
|
||||
} finally {
|
||||
connection.release()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConnection() {
|
||||
return pool.getConnection()
|
||||
}
|
||||
|
||||
export default pool
|
||||
`);
|
||||
console.log('Created src/db.js');
|
||||
|
||||
// src/jwt.js
|
||||
fs.writeFileSync(`${baseDir}/src/jwt.js`, `import jwt from 'jsonwebtoken'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
|
||||
const JWT_EXPIRY = process.env.JWT_EXPIRY || '15m'
|
||||
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '7d'
|
||||
|
||||
export function createAccessToken(userId) {
|
||||
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: JWT_EXPIRY })
|
||||
}
|
||||
|
||||
export function createRefreshToken() {
|
||||
return jwt.sign({ refreshToken: true }, JWT_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY })
|
||||
}
|
||||
|
||||
export function verifyToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeToken(token) {
|
||||
try {
|
||||
return jwt.decode(token)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
`);
|
||||
console.log('Created src/jwt.js');
|
||||
|
||||
// src/models.js
|
||||
fs.writeFileSync(`${baseDir}/src/models.js`, `import { query } from './db.js'
|
||||
import bcryptjs from 'bcryptjs'
|
||||
|
||||
// User model
|
||||
export const User = {
|
||||
async findById(userId) {
|
||||
const results = await query('SELECT id, email, username, created_at FROM users WHERE id = ?', [userId])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async findByEmail(email) {
|
||||
const results = await query('SELECT * FROM users WHERE email = ?', [email])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async findByUsername(username) {
|
||||
const results = await query('SELECT * FROM users WHERE username = ?', [username])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async create(email, username, passwordHash) {
|
||||
const id = crypto.randomUUID()
|
||||
await query('INSERT INTO users (id, email, username, password_hash) VALUES (?, ?, ?, ?)', [id, email, username, passwordHash])
|
||||
return this.findById(id)
|
||||
},
|
||||
|
||||
async verifyPassword(user, password) {
|
||||
return bcryptjs.compare(password, user.password_hash)
|
||||
},
|
||||
|
||||
async updateLastLogin(userId) {
|
||||
await query('UPDATE users SET last_login = NOW() WHERE id = ?', [userId])
|
||||
},
|
||||
}
|
||||
|
||||
// Session model
|
||||
export const Session = {
|
||||
async create(sessionId, eventId, createdBy, name) {
|
||||
await query('INSERT INTO photo_sessions (id, event_id, created_by, session_name) VALUES (?, ?, ?, ?)', [sessionId, eventId, createdBy, name])
|
||||
return this.findById(sessionId)
|
||||
},
|
||||
|
||||
async findById(sessionId) {
|
||||
const results = await query('SELECT * FROM photo_sessions WHERE id = ?', [sessionId])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async findByEvent(eventId) {
|
||||
return query('SELECT * FROM photo_sessions WHERE event_id = ? ORDER BY created_at DESC', [eventId])
|
||||
},
|
||||
|
||||
async update(sessionId, updates) {
|
||||
const fields = Object.keys(updates).map(k => \`\${k} = ?\`).join(', ')
|
||||
const values = Object.values(updates)
|
||||
values.push(sessionId)
|
||||
await query(\`UPDATE photo_sessions SET \${fields} WHERE id = ?\`, values)
|
||||
return this.findById(sessionId)
|
||||
},
|
||||
|
||||
async delete(sessionId) {
|
||||
await query('DELETE FROM photo_sessions WHERE id = ?', [sessionId])
|
||||
},
|
||||
}
|
||||
|
||||
// Photo model
|
||||
export const Photo = {
|
||||
async create(photoId, sessionId, fileName, filePath, fileSize, uploadedAt) {
|
||||
await query('INSERT INTO photos (id, session_id, file_name, file_path, file_size, uploaded_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[photoId, sessionId, fileName, filePath, fileSize, uploadedAt])
|
||||
return this.findById(photoId)
|
||||
},
|
||||
|
||||
async findById(photoId) {
|
||||
const results = await query('SELECT * FROM photos WHERE id = ?', [photoId])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async findBySession(sessionId) {
|
||||
return query('SELECT * FROM photos WHERE session_id = ? ORDER BY uploaded_at DESC', [sessionId])
|
||||
},
|
||||
|
||||
async delete(photoId) {
|
||||
await query('DELETE FROM photos WHERE id = ?', [photoId])
|
||||
},
|
||||
|
||||
async getCount(sessionId) {
|
||||
const results = await query('SELECT COUNT(*) as count FROM photos WHERE session_id = ?', [sessionId])
|
||||
return results[0]?.count || 0
|
||||
},
|
||||
}
|
||||
|
||||
// Event model
|
||||
export const Event = {
|
||||
async create(eventId, createdBy, eventName, description) {
|
||||
await query('INSERT INTO events (id, created_by, event_name, description) VALUES (?, ?, ?, ?)', [eventId, createdBy, eventName, description])
|
||||
return this.findById(eventId)
|
||||
},
|
||||
|
||||
async findById(eventId) {
|
||||
const results = await query('SELECT * FROM events WHERE id = ?', [eventId])
|
||||
return results[0] || null
|
||||
},
|
||||
|
||||
async findByUser(userId) {
|
||||
return query('SELECT * FROM events WHERE created_by = ? ORDER BY created_at DESC', [userId])
|
||||
},
|
||||
|
||||
async update(eventId, updates) {
|
||||
const fields = Object.keys(updates).map(k => \`\${k} = ?\`).join(', ')
|
||||
const values = Object.values(updates)
|
||||
values.push(eventId)
|
||||
await query(\`UPDATE events SET \${fields} WHERE id = ?\`, values)
|
||||
return this.findById(eventId)
|
||||
},
|
||||
|
||||
async delete(eventId) {
|
||||
await query('DELETE FROM events WHERE id = ?', [eventId])
|
||||
},
|
||||
}
|
||||
`);
|
||||
console.log('Created src/models.js');
|
||||
|
||||
// src/routes/auth.js
|
||||
fs.writeFileSync(`${baseDir}/src/routes/auth.js`, `import { Router } from 'express'
|
||||
import { User } from '../models.js'
|
||||
import { createAccessToken, createRefreshToken, verifyToken } from '../jwt.js'
|
||||
import bcryptjs from 'bcryptjs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Register
|
||||
router.post('/register', async (req, res) => {
|
||||
const { email, username, password } = req.body
|
||||
|
||||
if (!email || !username || !password) {
|
||||
return res.status(400).json({ error: 'Missing required fields' })
|
||||
}
|
||||
|
||||
try {
|
||||
const existingEmail = await User.findByEmail(email)
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ error: 'Email already registered' })
|
||||
}
|
||||
|
||||
const existingUser = await User.findByUsername(username)
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Username already taken' })
|
||||
}
|
||||
|
||||
const passwordHash = await bcryptjs.hash(password, 10)
|
||||
const user = await User.create(email, username, passwordHash)
|
||||
|
||||
const accessToken = createAccessToken(user.id)
|
||||
const refreshToken = createRefreshToken()
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password required' })
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findByEmail(email)
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' })
|
||||
}
|
||||
|
||||
const valid = await User.verifyPassword(user, password)
|
||||
if (!valid) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' })
|
||||
}
|
||||
|
||||
await User.updateLastLogin(user.id)
|
||||
|
||||
const accessToken = createAccessToken(user.id)
|
||||
const refreshToken = createRefreshToken()
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// Refresh token
|
||||
router.post('/refresh', (req, res) => {
|
||||
const { refreshToken } = req.body
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(400).json({ error: 'Refresh token required' })
|
||||
}
|
||||
|
||||
const decoded = verifyToken(refreshToken)
|
||||
if (!decoded || !decoded.userId) {
|
||||
return res.status(401).json({ error: 'Invalid refresh token' })
|
||||
}
|
||||
|
||||
const newAccessToken = createAccessToken(decoded.userId)
|
||||
res.json({ accessToken: newAccessToken })
|
||||
})
|
||||
|
||||
// Logout (stateless, just return success)
|
||||
router.post('/logout', (req, res) => {
|
||||
res.json({ message: 'Logged out successfully' })
|
||||
})
|
||||
|
||||
export default router
|
||||
`);
|
||||
console.log('Created src/routes/auth.js');
|
||||
|
||||
// src/routes/media.js
|
||||
fs.writeFileSync(`${baseDir}/src/routes/media.js`, `import { Router } from 'express'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Photo, Session } from '../models.js'
|
||||
import { authenticateToken } from '../middleware.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Configure multer for file uploads
|
||||
const uploadsDir = process.env.UPLOADS_DIR || './uploads'
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true })
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadsDir)
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname)
|
||||
const filename = \`\${uuidv4()}\${ext}\`
|
||||
cb(null, filename)
|
||||
},
|
||||
})
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10485760 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||
if (allowedMimes.includes(file.mimetype)) {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error('Invalid file type'))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Upload photo
|
||||
router.post('/upload/:sessionId', authenticateToken, upload.single('photo'), async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params
|
||||
|
||||
const session = await Session.findById(sessionId)
|
||||
if (!session) {
|
||||
if (req.file) fs.unlinkSync(req.file.path)
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
const photoId = uuidv4()
|
||||
const photo = await Photo.create(
|
||||
photoId,
|
||||
sessionId,
|
||||
req.file.originalname,
|
||||
req.file.path,
|
||||
req.file.size,
|
||||
new Date()
|
||||
)
|
||||
|
||||
res.json(photo)
|
||||
} catch (error) {
|
||||
if (req.file) fs.unlinkSync(req.file.path)
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// Get photos for session
|
||||
router.get('/session/:sessionId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params
|
||||
const photos = await Photo.findBySession(sessionId)
|
||||
res.json(photos)
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// Delete photo
|
||||
router.delete('/:photoId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { photoId } = req.params
|
||||
|
||||
const photo = await Photo.findById(photoId)
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Photo not found' })
|
||||
}
|
||||
|
||||
if (fs.existsSync(photo.file_path)) {
|
||||
fs.unlinkSync(photo.file_path)
|
||||
}
|
||||
|
||||
await Photo.delete(photoId)
|
||||
res.json({ message: 'Photo deleted' })
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
`);
|
||||
console.log('Created src/routes/media.js');
|
||||
|
||||
// src/middleware.js
|
||||
fs.writeFileSync(`${baseDir}/src/middleware.js`, `import { verifyToken } from './jwt.js'
|
||||
|
||||
export function authenticateToken(req, res, next) {
|
||||
const authHeader = req.headers['authorization']
|
||||
const token = authHeader && authHeader.split(' ')[1]
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' })
|
||||
}
|
||||
|
||||
const decoded = verifyToken(token)
|
||||
if (!decoded) {
|
||||
return res.status(403).json({ error: 'Invalid or expired token' })
|
||||
}
|
||||
|
||||
req.userId = decoded.userId
|
||||
next()
|
||||
}
|
||||
|
||||
export function errorHandler(err, req, res, next) {
|
||||
console.error(err)
|
||||
res.status(err.status || 500).json({ error: err.message || 'Internal server error' })
|
||||
}
|
||||
`);
|
||||
console.log('Created src/middleware.js');
|
||||
|
||||
// src/server.js
|
||||
fs.writeFileSync(`${baseDir}/src/server.js`, `import express from 'express'
|
||||
import cors from 'cors'
|
||||
import 'express-async-errors'
|
||||
import dotenv from 'dotenv'
|
||||
import authRoutes from './routes/auth.js'
|
||||
import mediaRoutes from './routes/media.js'
|
||||
import { errorHandler } from './middleware.js'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
credentials: true,
|
||||
}))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/media', mediaRoutes)
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok' })
|
||||
})
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler)
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Not found' })
|
||||
})
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(\`Server running on port \${PORT}\`)
|
||||
})
|
||||
`);
|
||||
console.log('Created src/server.js');
|
||||
|
||||
console.log('\\n✅ Backend structure created successfully!');
|
||||
console.log('Next steps:');
|
||||
console.log(' cd backend');
|
||||
console.log(' npm install');
|
||||
console.log(' cp .env.example .env');
|
||||
console.log(' npm run dev');
|
||||
Reference in New Issue
Block a user