refactor: migrate database from SQLite to PostgreSQL
This commit is contained in:
+78
-61
@@ -1,70 +1,87 @@
|
|||||||
const sqlite3 = require('sqlite3').verbose();
|
const { Pool } = require('pg');
|
||||||
const path = require('path');
|
require('dotenv').config();
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const dbPath = path.join(__dirname, 'data', 'database.sqlite');
|
// Coolify uses DATABASE_URL
|
||||||
const dataDir = path.dirname(dbPath);
|
// If not set, we can fallback to a local postgres or just let it fail
|
||||||
|
const pool = new Pool({
|
||||||
if (!fs.existsSync(dataDir)) {
|
connectionString: process.env.DATABASE_URL,
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = new sqlite3.Database(dbPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error opening database', err.message);
|
|
||||||
} else {
|
|
||||||
console.log('Connected to the SQLite database.');
|
|
||||||
db.run(`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 INTEGER DEFAULT 0
|
|
||||||
)`, (err) => {
|
|
||||||
// Attempt to add column for existing databases (fails silently if exists)
|
|
||||||
if (!err) {
|
|
||||||
db.run('ALTER TABLE profiles ADD COLUMN is_active INTEGER DEFAULT 0', () => {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS projects (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT,
|
|
||||||
name TEXT,
|
|
||||||
scene_data TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES profiles (id)
|
|
||||||
)`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const run = (sql, params = []) => {
|
pool.on('error', (err, client) => {
|
||||||
return new Promise((resolve, reject) => {
|
console.error('Unexpected error on idle client', err);
|
||||||
db.run(sql, params, function (err) {
|
process.exit(-1);
|
||||||
if (err) reject(err);
|
});
|
||||||
else resolve({ id: this.lastID, changes: this.changes });
|
|
||||||
});
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const get = (sql, params = []) => {
|
initDB();
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.get(sql, params, (err, result) => {
|
// Wrapper functions to simulate the old sqlite API but backed by pg
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(result);
|
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 all = (sql, params = []) => {
|
const get = async (sql, params = []) => {
|
||||||
return new Promise((resolve, reject) => {
|
let paramIndex = 1;
|
||||||
db.all(sql, params, (err, rows) => {
|
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(rows);
|
const res = await pool.query(pgSql, params);
|
||||||
});
|
return res.rows[0];
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { db, run, get, all };
|
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 };
|
||||||
|
|||||||
Generated
+129
-1020
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "^9.0.5",
|
"nodemailer": "^9.0.5",
|
||||||
"sqlite3": "^5.1.7",
|
"pg": "^8.23.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -64,15 +64,15 @@ app.post('/api/auth/register', async (req, res) => {
|
|||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
// First user becomes admin and is automatically active
|
// First user becomes admin and is automatically active
|
||||||
const countRow = await get('SELECT COUNT(*) as count FROM profiles');
|
const countRow = await get('SELECT COUNT(*) as count FROM profiles');
|
||||||
const role = countRow.count === 0 ? 'admin' : 'user';
|
const role = parseInt(countRow.count) === 0 ? 'admin' : 'user';
|
||||||
const is_active = countRow.count === 0 ? 1 : 0;
|
const is_active = parseInt(countRow.count) === 0 ? true : false;
|
||||||
|
|
||||||
await run(
|
await run(
|
||||||
'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
[id, email, hash, role, 10, 500, is_active]
|
[id, email, hash, role, 10, 500, is_active]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (is_active === 0) {
|
if (is_active === false) {
|
||||||
return res.json({ status: 'pending', message: 'Account created. Please contact an administrator to activate your account.' });
|
return res.json({ status: 'pending', message: 'Account created. Please contact an administrator to activate your account.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
const valid = await bcrypt.compare(password, user.password_hash);
|
const valid = await bcrypt.compare(password, user.password_hash);
|
||||||
if (!valid) return res.status(400).json({ error: 'Invalid credentials' });
|
if (!valid) return res.status(400).json({ error: 'Invalid credentials' });
|
||||||
|
|
||||||
if (user.is_active === 0) {
|
if (user.is_active === false) {
|
||||||
return res.status(403).json({ error: 'Account pending admin approval. Please contact an administrator.' });
|
return res.status(403).json({ error: 'Account pending admin approval. Please contact an administrator.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ app.put('/api/users/:id', authenticate, async (req, res) => {
|
|||||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
||||||
const { role, api_credits, storage_limit_mb, is_active } = req.body;
|
const { role, api_credits, storage_limit_mb, is_active } = req.body;
|
||||||
try {
|
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]);
|
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 });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
|
|||||||
Reference in New Issue
Block a user