const express = require('express'); const cors = require('cors'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { db, run, get, all } = require('./database'); const app = express(); app.use(cors()); app.use(express.json({ limit: '100mb' })); const JWT_SECRET = 'viz-3d-local-secret-key-1234'; // Ensure uploads dir exists const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } app.use('/uploads', express.static(uploadsDir)); // Multer storage const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, uploadsDir); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + '-' + file.originalname); } }); const upload = multer({ storage: storage }); // --- AUTH MIDDLEWARE --- const authenticate = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) return res.status(401).json({ error: 'No token provided' }); const token = authHeader.split(' ')[1]; jwt.verify(token, JWT_SECRET, (err, decoded) => { if (err) return res.status(401).json({ error: 'Invalid token' }); req.user = decoded; next(); }); }; // --- ROUTES --- // 1. Auth Register app.post('/api/auth/register', async (req, res) => { const { email, password } = req.body; if (!email || !password) return res.status(400).json({ error: 'Email and password required' }); try { const existing = await get('SELECT * FROM profiles WHERE email = ?', [email]); if (existing) return res.status(400).json({ error: 'User already exists' }); const hash = await bcrypt.hash(password, 10); const id = uuidv4(); // First user becomes admin and is automatically active const countRow = await get('SELECT COUNT(*) as count FROM profiles'); const role = countRow.count === 0 ? 'admin' : 'user'; const is_active = countRow.count === 0 ? 1 : 0; await run( 'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)', [id, email, hash, role, 10, 500, is_active] ); if (is_active === 0) { return res.json({ status: 'pending', message: 'Account created. Please contact an administrator to activate your account.' }); } const token = jwt.sign({ id, email, role }, JWT_SECRET, { expiresIn: '7d' }); const profile = await get('SELECT id, email, role, api_credits, storage_limit_mb, is_active FROM profiles WHERE id = ?', [id]); res.json({ status: 'active', token, profile }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 2. Auth Login app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; try { const user = await get('SELECT * FROM profiles WHERE email = ?', [email]); if (!user) return res.status(400).json({ error: 'Invalid credentials' }); const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return res.status(400).json({ error: 'Invalid credentials' }); if (user.is_active === 0) { return res.status(403).json({ error: 'Account pending admin approval. Please contact an administrator.' }); } const token = jwt.sign({ id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '7d' }); const profile = { id: user.id, email: user.email, role: user.role, api_credits: user.api_credits, storage_limit_mb: user.storage_limit_mb, is_active: user.is_active }; res.json({ token, profile }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 3. Auth Me app.get('/api/auth/me', authenticate, async (req, res) => { try { const profile = await get('SELECT id, email, role, api_credits, storage_limit_mb, is_active FROM profiles WHERE id = ?', [req.user.id]); res.json({ profile }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 4. Get all projects for user app.get('/api/projects', authenticate, async (req, res) => { try { const projects = await all('SELECT id, name FROM projects WHERE user_id = ?', [req.user.id]); res.json({ projects }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 5. Get project by ID app.get('/api/projects/:id', authenticate, async (req, res) => { try { const project = await get('SELECT * FROM projects WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]); if (!project) return res.status(404).json({ error: 'Project not found' }); if (project.scene_data) { project.scene_data = JSON.parse(project.scene_data); } res.json({ project }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 6. Save (create or update) project app.post('/api/projects', authenticate, async (req, res) => { const { id, name, scene_data } = req.body; const projectId = id || uuidv4(); try { const existing = await get('SELECT * FROM projects WHERE id = ?', [projectId]); if (existing) { if (existing.user_id !== req.user.id) return res.status(403).json({ error: 'Forbidden' }); await run('UPDATE projects SET name = ?, scene_data = ? WHERE id = ?', [name, JSON.stringify(scene_data), projectId]); } else { await run('INSERT INTO projects (id, user_id, name, scene_data) VALUES (?, ?, ?, ?)', [projectId, req.user.id, name, JSON.stringify(scene_data)]); } res.json({ success: true, id: projectId }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 7. Get users (Admin only) app.get('/api/users', authenticate, async (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); try { const users = await all('SELECT id, email, role, api_credits, storage_limit_mb, is_active FROM profiles'); res.json({ users }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 8. Update user (Admin only) app.put('/api/users/:id', authenticate, async (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); const { role, api_credits, storage_limit_mb, is_active } = req.body; try { await run('UPDATE profiles SET role = ?, api_credits = ?, storage_limit_mb = ?, is_active = ? WHERE id = ?', [role, api_credits, storage_limit_mb, is_active !== undefined ? is_active : 1, req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 9. Deduct API credit app.post('/api/users/deduct-credit', authenticate, async (req, res) => { try { const user = await get('SELECT api_credits FROM profiles WHERE id = ?', [req.user.id]); if (user.api_credits <= 0) return res.status(400).json({ error: 'Not enough credits' }); await run('UPDATE profiles SET api_credits = api_credits - 1 WHERE id = ?', [req.user.id]); const updated = await get('SELECT api_credits FROM profiles WHERE id = ?', [req.user.id]); res.json({ api_credits: updated.api_credits }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 10. Upload file app.post('/api/upload', authenticate, upload.single('file'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const url = `${req.protocol}://${req.get('host')}/uploads/${req.file.filename}`; res.json({ url }); }); const PORT = process.env.PORT || 3005; app.listen(PORT, () => { console.log(`Backend server running on port ${PORT}`); });