chore: flatten repository to completely erase 255MB history
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,87 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
// Coolify uses DATABASE_URL
|
||||
// If not set, we can fallback to a local postgres or just let it fail
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
pool.on('error', (err, client) => {
|
||||
console.error('Unexpected error on idle client', err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const initDB = async () => {
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
console.log('Connected to the PostgreSQL database.');
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE,
|
||||
password_hash TEXT,
|
||||
role TEXT DEFAULT 'user',
|
||||
api_credits INTEGER DEFAULT 10,
|
||||
storage_limit_mb INTEGER DEFAULT 500,
|
||||
is_active BOOLEAN DEFAULT false
|
||||
)
|
||||
`);
|
||||
|
||||
// In Postgres, ALTER TABLE ADD COLUMN IF NOT EXISTS requires PG >= 9.6
|
||||
// So we can catch the error if column exists just like sqlite logic
|
||||
try {
|
||||
await client.query(`ALTER TABLE profiles ADD COLUMN is_active BOOLEAN DEFAULT false`);
|
||||
} catch (err) {
|
||||
// Ignore if it already exists
|
||||
}
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT REFERENCES profiles(id),
|
||||
name TEXT,
|
||||
scene_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
client.release();
|
||||
} catch (err) {
|
||||
console.error('Error initializing database:', err);
|
||||
}
|
||||
};
|
||||
|
||||
initDB();
|
||||
|
||||
// Wrapper functions to simulate the old sqlite API but backed by pg
|
||||
|
||||
const run = async (sql, params = []) => {
|
||||
// convert ? to $1, $2, etc.
|
||||
let paramIndex = 1;
|
||||
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
|
||||
|
||||
const res = await pool.query(pgSql, params);
|
||||
// `run` in sqlite returns { id: this.lastID, changes: this.changes }
|
||||
// Postgres doesn't return lastID unless RETURNING is used, but for uuids it doesn't matter.
|
||||
return { changes: res.rowCount };
|
||||
};
|
||||
|
||||
const get = async (sql, params = []) => {
|
||||
let paramIndex = 1;
|
||||
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
|
||||
|
||||
const res = await pool.query(pgSql, params);
|
||||
return res.rows[0];
|
||||
};
|
||||
|
||||
const all = async (sql, params = []) => {
|
||||
let paramIndex = 1;
|
||||
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
|
||||
|
||||
const res = await pool.query(pgSql, params);
|
||||
return res.rows;
|
||||
};
|
||||
|
||||
module.exports = { db: pool, run, get, all };
|
||||
@@ -0,0 +1,46 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'smtp.office365.com',
|
||||
port: parseInt(process.env.SMTP_PORT || '587', 10),
|
||||
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports (587 uses STARTTLS)
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
ciphers: 'SSLv3', // sometimes required for Office365
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sends an email using the configured SMTP server.
|
||||
* @param {string} to - Recipient email address
|
||||
* @param {string} subject - Email subject
|
||||
* @param {string} text - Plain text body
|
||||
* @param {string} html - HTML body (optional)
|
||||
* @returns {Promise<any>} Info object from nodemailer
|
||||
*/
|
||||
const sendMail = async (to, subject, text, html = '') => {
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
});
|
||||
console.log(`Email sent to ${to}: ${info.messageId}`);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendMail,
|
||||
transporter
|
||||
};
|
||||
Generated
+1914
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "titan3d-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Local API for Titan3d",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^9.0.5",
|
||||
"pg": "^8.23.0",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
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 { 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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
|
||||
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 = `${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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user