@@ -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');
|
||||
@@ -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
|
||||
@@ -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
@@ -5,79 +5,44 @@ MYSQL_ROOT_USER="${MYSQL_ROOT_USER:-root}"
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
|
||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_DATABASE="${MYSQL_DATABASE:-photobooth}"
|
||||
MYSQL_APP_USER="${MYSQL_APP_USER:-photobooth_app}"
|
||||
MYSQL_APP_PASSWORD="${MYSQL_APP_PASSWORD:-}"
|
||||
MARKER_FILE="${MARKER_FILE:-/var/lib/photobooth/db-setup.done}"
|
||||
|
||||
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
|
||||
echo "MYSQL_ROOT_PASSWORD is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$MYSQL_APP_PASSWORD" ]; then
|
||||
echo "MYSQL_APP_PASSWORD is required." >&2
|
||||
if [ -z "$MYSQL_ROOT_PASSWORD" ] || [ -z "$MYSQL_APP_PASSWORD" ]; then
|
||||
echo "Error: MYSQL_ROOT_PASSWORD and MYSQL_APP_PASSWORD required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$MARKER_FILE" ]; then
|
||||
echo "Database already initialized: $MARKER_FILE"
|
||||
echo "Database already initialized."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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 \
|
||||
--protocol=TCP \
|
||||
-h "$MYSQL_HOST" \
|
||||
-P "$MYSQL_PORT" \
|
||||
-u "$MYSQL_ROOT_USER" <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\`
|
||||
CHARACTER SET utf8mb4
|
||||
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
|
||||
-u "$MYSQL_ROOT_USER" < "$TEMP_SQL" || {
|
||||
echo "Error: Database setup failed" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
touch "$MARKER_FILE"
|
||||
echo "Database initialized."
|
||||
echo "Database setup completed."
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user