Files
titan3d_react_node_sqllite/backend/server.js
T
2026-09-02 14:14:57 +05:30

501 lines
18 KiB
JavaScript

require('dotenv').config();
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 pdfParse = require('pdf-parse');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const axios = require('axios');
const { db, run, get, all } = require('./database');
const { sendMail } = require('./mail');
const app = express();
app.use(cors());
app.use(express.json({ limit: '500mb' }));
app.use(express.urlencoded({ limit: '500mb', extended: true }));
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 = parseInt(countRow.count) === 0 ? 'admin' : 'user';
const is_active = parseInt(countRow.count) === 0 ? true : false;
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 === false) {
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 === false) {
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 });
}
});
// 3.5. Get User API Keys
app.get('/api/user/keys', authenticate, async (req, res) => {
try {
const profile = await get('SELECT api_keys FROM profiles WHERE id = ?', [req.user.id]);
let keys = {};
if (profile && profile.api_keys) {
try {
keys = JSON.parse(profile.api_keys);
} catch (e) {
// ignore invalid json
}
}
res.json({ keys });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 3.6. Update User API Keys
app.put('/api/user/keys', authenticate, async (req, res) => {
const { keys } = req.body;
if (!keys || typeof keys !== 'object') {
return res.status(400).json({ error: 'Invalid keys object' });
}
try {
const keysStr = JSON.stringify(keys);
await run('UPDATE profiles SET api_keys = ? WHERE id = ?', [keysStr, req.user.id]);
res.json({ success: true });
} 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, created_at 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 });
}
});
app.delete('/api/projects/:id', authenticate, async (req, res) => {
const { id } = req.params;
try {
const existing = await get('SELECT * FROM projects WHERE id = ?', [id]);
if (!existing) return res.status(404).json({ error: 'Not found' });
if (existing.user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
await run('DELETE FROM projects WHERE id = ?', [id]);
res.json({ success: true });
} 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 : true, 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 = `/uploads/${req.file.filename}`;
res.json({ url });
});
// 10b. Chunked Upload - Receive chunk
app.post('/api/upload/chunk', authenticate, upload.single('file'), (req, res) => {
const { uploadId, chunkIndex, totalChunks } = req.body;
if (!req.file || !uploadId || !chunkIndex || !totalChunks) {
return res.status(400).json({ error: 'Missing chunk data' });
}
const tempDir = path.join(uploadsDir, 'temp', uploadId);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const chunkPath = path.join(tempDir, chunkIndex);
fs.renameSync(req.file.path, chunkPath);
res.json({ success: true });
});
// 10c. Chunked Upload - Complete
app.post('/api/upload/complete', authenticate, (req, res) => {
const { uploadId, fileName, totalChunks } = req.body;
if (!uploadId || !fileName || !totalChunks) {
return res.status(400).json({ error: 'Missing completion data' });
}
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const finalFileName = uniqueSuffix + '-' + fileName;
const finalPath = path.join(uploadsDir, finalFileName);
const tempDir = path.join(uploadsDir, 'temp', uploadId);
try {
const writeStream = fs.createWriteStream(finalPath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(tempDir, i.toString());
if (!fs.existsSync(chunkPath)) {
throw new Error(`Chunk ${i} is missing`);
}
const data = fs.readFileSync(chunkPath);
writeStream.write(data);
}
writeStream.end();
// Clean up temp dir
fs.rmSync(tempDir, { recursive: true, force: true });
const url = `/uploads/${finalFileName}`;
res.json({ url });
} catch (error) {
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
res.status(500).json({ error: error.message });
}
});
// ==========================================
// SPARK PLUG WORKFLOW APIS
// ==========================================
const getKeysFromProfile = async (userId) => {
const profile = await get('SELECT api_keys FROM profiles WHERE id = ?', [userId]);
if (profile && profile.api_keys) {
try {
return JSON.parse(profile.api_keys);
} catch(e) {}
}
return {};
};
// 1. Extract PDF (Gemini)
app.post('/api/sparkplug/extract', authenticate, upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
try {
const keys = await getKeysFromProfile(req.user.id);
const geminiKey = keys.GEMINI_API_KEY;
if (!geminiKey) return res.status(400).json({ error: 'Gemini API key missing' });
// Parse PDF
const dataBuffer = fs.readFileSync(req.file.path);
const base64Pdf = dataBuffer.toString('base64');
// Call Gemini to extract prompt
const genAI = new GoogleGenerativeAI(geminiKey);
const model = genAI.getGenerativeModel({ model: "gemini-3.5-flash-lite" });
const prompt = `
You are an expert product designer. Review the attached product spec PDF (which may include text and sample images).
Your goal is to create a highly detailed, concise visual design prompt for an image generation AI.
CRITICAL: If there is a sample image of the product in the PDF, you MUST carefully analyze it and extract the exact HEX color codes used in the design.
Include these HEX codes, as well as the exact shape, materials, typography style, and label content in your final prompt so the image generator knows exactly how it looks.
Do not include background details.
`;
const result = await model.generateContent([
{
inlineData: {
data: base64Pdf,
mimeType: "application/pdf"
}
},
prompt
]);
const response = await result.response;
const extractedPrompt = response.text();
res.json({ prompt: extractedPrompt });
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
if (req.file) {
fs.unlinkSync(req.file.path); // cleanup uploaded PDF
}
}
});
// 2. Generate Images (Gemini Imagen 3)
app.post('/api/sparkplug/generate-images', authenticate, async (req, res) => {
const { prompt } = req.body;
if (!prompt) return res.status(400).json({ error: 'No prompt provided' });
try {
const keys = await getKeysFromProfile(req.user.id);
const apiKey = keys.GEMINI_API_KEY || keys.NANO_BANANA_API_KEY;
if (!apiKey) return res.status(400).json({ error: 'Gemini API key missing' });
// Use the standard Gemini Imagen 3 API
const baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-images:predict';
const views = ['Front view', 'Back view', 'Left side view', 'Right side view'];
// Run parallel generation for all 4 views
const promises = views.map(async (view, index) => {
const fullPrompt = `${prompt}. ${view}, isolated on a pure white background, studio lighting.`;
const payload = {
instances: [
{ prompt: fullPrompt }
],
parameters: {
sampleCount: 1,
aspectRatio: "1:1"
}
};
const response = await axios.post(`${baseUrl}?key=${apiKey}`, payload, {
headers: { 'Content-Type': 'application/json' }
});
let base64Image = null;
if (response.data && response.data.predictions && response.data.predictions.length > 0) {
base64Image = response.data.predictions[0].bytesBase64Encoded;
}
if (!base64Image) {
throw new Error(`Could not extract base64 image data from Gemini response: ${JSON.stringify(response.data)}`);
}
const buffer = Buffer.from(base64Image, 'base64');
const filename = `sparkplug-${req.user.id}-${Date.now()}-${index}.png`;
const filepath = path.join(uploadsDir, filename);
fs.writeFileSync(filepath, buffer);
return `/uploads/${filename}`;
});
const generatedUrls = await Promise.all(promises);
res.json({ images: generatedUrls });
} catch (err) {
let errorMsg = err.message;
if (err.response && err.response.data) {
errorMsg = typeof err.response.data === 'object' ? JSON.stringify(err.response.data) : err.response.data;
}
console.error("Gemini Image Gen Error:", errorMsg);
res.status(500).json({ error: errorMsg });
}
});
// 3. Generate 3D (Meshy)
app.post('/api/sparkplug/generate-3d', authenticate, async (req, res) => {
const { imageUrls } = req.body;
if (!imageUrls || imageUrls.length === 0) return res.status(400).json({ error: 'No images provided' });
try {
const keys = await getKeysFromProfile(req.user.id);
const meshyKey = keys.MESHY_API_KEY;
if (!meshyKey) return res.status(400).json({ error: 'Meshy API key missing' });
// Assuming imageUrls are local paths like /uploads/...
// Meshy requires base64 Data URIs if the images are not publicly accessible URLs.
// Our local URLs are not publicly accessible to Meshy's servers! We MUST convert to base64.
const base64Images = imageUrls.map(url => {
// Extract filename from URL
const filename = url.replace('/uploads/', '');
const filepath = path.join(uploadsDir, filename);
const buffer = fs.readFileSync(filepath);
return `data:image/png;base64,${buffer.toString('base64')}`;
});
const response = await axios.post('https://api.meshy.ai/openapi/v1/multi-image-to-3d', {
image_urls: base64Images,
enable_pbr: true
}, {
headers: {
'Authorization': `Bearer ${meshyKey}`,
'Content-Type': 'application/json'
}
});
res.json({ taskId: response.data.result });
} catch (err) {
const errorMsg = err.response?.data?.message || err.message;
res.status(500).json({ error: errorMsg });
}
});
// 4. Check 3D Status (Meshy)
app.get('/api/sparkplug/status-3d/:taskId', authenticate, async (req, res) => {
const { taskId } = req.params;
try {
const keys = await getKeysFromProfile(req.user.id);
const meshyKey = keys.MESHY_API_KEY;
if (!meshyKey) return res.status(400).json({ error: 'Meshy API key missing' });
const response = await axios.get(`https://api.meshy.ai/openapi/v1/multi-image-to-3d/${taskId}`, {
headers: {
'Authorization': `Bearer ${meshyKey}`
}
});
res.json(response.data);
} catch (err) {
const errorMsg = err.response?.data?.message || err.message;
res.status(500).json({ error: errorMsg });
}
});
const PORT = process.env.PORT || 3005;
app.listen(PORT, () => {
console.log(`Backend server running on port ${PORT}`);
});