database
Deploy photobooth / deploy (push) Has been cancelled

This commit is contained in:
2026-05-26 10:14:31 +07:00
parent 100e5c3a87
commit 79b959aac7
4 changed files with 669 additions and 59 deletions
+562
View File
@@ -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');
+36
View File
@@ -0,0 +1,36 @@
@echo off
REM Create backend directory structure
echo Creating backend directory structure...
mkdir backend\src\routes 2>nul
REM Create package.json
(
echo {
echo "name": "photobooth-backend",
echo "version": "1.0.0",
echo "type": "module",
echo "main": "src/server.js",
echo "scripts": {
echo "start": "node src/server.js",
echo "dev": "node --watch src/server.js"
echo },
echo "keywords": ["photobooth", "express", "mysql"],
echo "author": "",
echo "license": "MIT",
echo "dependencies": {
echo "express": "^4.18.2",
echo "cors": "^2.8.5",
echo "dotenv": "^16.3.1",
echo "mysql2": "^3.6.5",
echo "jsonwebtoken": "^9.1.0",
echo "bcryptjs": "^2.4.3",
echo "uuid": "^9.0.1",
echo "express-async-errors": "^3.1.1",
echo "multer": "^1.4.5-lts.1"
echo },
echo "devDependencies": {}
echo }
) > backend\package.json
echo.
echo Backend structure created. Now run: cd backend && npm install
+47
View File
@@ -0,0 +1,47 @@
-- Photobooth database setup
CREATE DATABASE IF NOT EXISTS `photobooth`
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE USER IF NOT EXISTS 'photobooth_app'@'%' IDENTIFIED BY 'photobooth_app_password_placeholder';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON `photobooth`.* TO 'photobooth_app'@'%';
FLUSH PRIVILEGES;
USE `photobooth`;
CREATE TABLE IF NOT EXISTS users (
id CHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(120) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS refresh_tokens (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
token_hash VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_refresh_token_hash (token_hash),
KEY idx_refresh_user (user_id),
CONSTRAINT fk_refresh_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS media (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
type ENUM('shot','strip') NOT NULL,
url VARCHAR(2048) NOT NULL,
thumb_url VARCHAR(2048) NULL,
theme_id VARCHAR(64) NULL,
meta JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_media_user_created (user_id, created_at),
KEY idx_media_type (type),
CONSTRAINT fk_media_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
+24 -59
View File
@@ -5,79 +5,44 @@ MYSQL_ROOT_USER="${MYSQL_ROOT_USER:-root}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
MYSQL_PORT="${MYSQL_PORT:-3306}" MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_DATABASE="${MYSQL_DATABASE:-photobooth}"
MYSQL_APP_USER="${MYSQL_APP_USER:-photobooth_app}"
MYSQL_APP_PASSWORD="${MYSQL_APP_PASSWORD:-}" MYSQL_APP_PASSWORD="${MYSQL_APP_PASSWORD:-}"
MARKER_FILE="${MARKER_FILE:-/var/lib/photobooth/db-setup.done}" MARKER_FILE="${MARKER_FILE:-/var/lib/photobooth/db-setup.done}"
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then if [ -z "$MYSQL_ROOT_PASSWORD" ] || [ -z "$MYSQL_APP_PASSWORD" ]; then
echo "MYSQL_ROOT_PASSWORD is required." >&2 echo "Error: MYSQL_ROOT_PASSWORD and MYSQL_APP_PASSWORD required" >&2
exit 1
fi
if [ -z "$MYSQL_APP_PASSWORD" ]; then
echo "MYSQL_APP_PASSWORD is required." >&2
exit 1 exit 1
fi fi
if [ -f "$MARKER_FILE" ]; then if [ -f "$MARKER_FILE" ]; then
echo "Database already initialized: $MARKER_FILE" echo "Database already initialized."
exit 0 exit 0
fi fi
mkdir -p "$(dirname "$MARKER_FILE")" mkdir -p "$(dirname "$MARKER_FILE")"
# Find mysql command
if ! command -v mysql &> /dev/null; then
echo "Error: mysql not found in PATH" >&2
exit 1
fi
# Create temp SQL file with actual password
TEMP_SQL=$(mktemp)
trap "rm -f $TEMP_SQL" EXIT
sed "s/photobooth_app_password_placeholder/$MYSQL_APP_PASSWORD/g" \
"$(dirname "$0")/photobooth.sql" > "$TEMP_SQL"
# Execute
MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql \ MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql \
--protocol=TCP \
-h "$MYSQL_HOST" \ -h "$MYSQL_HOST" \
-P "$MYSQL_PORT" \ -P "$MYSQL_PORT" \
-u "$MYSQL_ROOT_USER" <<SQL -u "$MYSQL_ROOT_USER" < "$TEMP_SQL" || {
CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` echo "Error: Database setup failed" >&2
CHARACTER SET utf8mb4 exit 1
COLLATE utf8mb4_0900_ai_ci; }
CREATE USER IF NOT EXISTS '$MYSQL_APP_USER'@'%' IDENTIFIED BY '$MYSQL_APP_PASSWORD';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_APP_USER'@'%';
FLUSH PRIVILEGES;
USE \`$MYSQL_DATABASE\`;
CREATE TABLE IF NOT EXISTS users (
id CHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(120) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS refresh_tokens (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
token_hash VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_refresh_token_hash (token_hash),
KEY idx_refresh_user (user_id),
CONSTRAINT fk_refresh_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS media (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
type ENUM('shot','strip') NOT NULL,
url VARCHAR(2048) NOT NULL,
thumb_url VARCHAR(2048) NULL,
theme_id VARCHAR(64) NULL,
meta JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_media_user_created (user_id, created_at),
KEY idx_media_type (type),
CONSTRAINT fk_media_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
SQL
touch "$MARKER_FILE" touch "$MARKER_FILE"
echo "Database initialized." echo "Database setup completed."