Slefhostet und postgres

This commit is contained in:
2026-04-02 11:39:57 +02:00
parent b1c99893a6
commit 08483c7075
215 changed files with 4584 additions and 5190 deletions

View File

@@ -1,5 +1,5 @@
const crypto = require('crypto');
const { get, run } = require('./sqlite');
const { get, run } = require('./postgres');
const JWT_SECRET = process.env.JWT_SECRET || 'greenlens-dev-secret-change-in-prod';
const TOKEN_EXPIRY_SECONDS = 365 * 24 * 3600; // 1 year
@@ -52,47 +52,51 @@ const hashPassword = (password) =>
// ─── Schema ────────────────────────────────────────────────────────────────
const ensureAuthSchema = async (db) => {
await run(
db,
`CREATE TABLE IF NOT EXISTS auth_users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
name TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
);
};
const ensureAuthSchema = async (db) => {
await run(
db,
`CREATE TABLE IF NOT EXISTS auth_users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
);
};
// ─── Operations ───────────────────────────────────────────────────────────
const signUp = async (db, email, name, password) => {
const normalizedEmail = email.trim().toLowerCase();
const existing = await get(db, 'SELECT id FROM auth_users WHERE email = ?', [normalizedEmail]);
if (existing) {
const err = new Error('Email already in use.');
err.code = 'EMAIL_TAKEN';
err.status = 409;
throw err;
}
const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
await run(db, 'INSERT INTO auth_users (id, email, name, password_hash) VALUES (?, ?, ?, ?)', [
id,
normalizedEmail,
name.trim(),
hashPassword(password),
]);
const signUp = async (db, email, name, password) => {
const normalizedEmail = email.trim().toLowerCase();
const existing = await get(db, 'SELECT id FROM auth_users WHERE LOWER(email) = LOWER($1)', [normalizedEmail]);
if (existing) {
const err = new Error('Email already in use.');
err.code = 'EMAIL_TAKEN';
err.status = 409;
throw err;
}
const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
await run(db, 'INSERT INTO auth_users (id, email, name, password_hash) VALUES ($1, $2, $3, $4)', [
id,
normalizedEmail,
name.trim(),
hashPassword(password),
]);
return { id, email: normalizedEmail, name: name.trim() };
};
const login = async (db, email, password) => {
const normalizedEmail = email.trim().toLowerCase();
const user = await get(db, 'SELECT id, email, name, password_hash FROM auth_users WHERE email = ?', [normalizedEmail]);
if (!user) {
const err = new Error('No account found for this email.');
err.code = 'USER_NOT_FOUND';
err.status = 401;
const login = async (db, email, password) => {
const normalizedEmail = email.trim().toLowerCase();
const user = await get(
db,
'SELECT id, email, name, password_hash FROM auth_users WHERE LOWER(email) = LOWER($1)',
[normalizedEmail],
);
if (!user) {
const err = new Error('No account found for this email.');
err.code = 'USER_NOT_FOUND';
err.status = 401;
throw err;
}
if (user.password_hash !== hashPassword(password)) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

93
server/lib/postgres.js Normal file
View File

@@ -0,0 +1,93 @@
const { Pool } = require('pg');
const parseBoolean = (value, fallback = false) => {
if (typeof value !== 'string') return fallback;
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
};
const buildDatabaseUrlFromParts = () => {
const host = (process.env.POSTGRES_HOST || 'postgres').trim();
const port = Number(process.env.POSTGRES_PORT || 5432);
const database = (process.env.POSTGRES_DB || 'greenlns').trim();
const user = (process.env.POSTGRES_USER || 'greenlns').trim();
const password = process.env.POSTGRES_PASSWORD;
if (!password) {
return '';
}
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(database)}`;
};
const getDefaultDbPath = () => {
return (process.env.DATABASE_URL || buildDatabaseUrlFromParts()).trim();
};
const getPoolConfig = () => {
const connectionString = getDefaultDbPath();
if (!connectionString) {
throw new Error('DATABASE_URL or POSTGRES_* environment variables are required.');
}
const sslEnabled = parseBoolean(process.env.DATABASE_SSL, false);
return {
connectionString,
max: Number(process.env.PGPOOL_MAX || 10),
ssl: sslEnabled ? { rejectUnauthorized: false } : false,
};
};
const translateSql = (sql) => {
if (typeof sql !== 'string') return sql;
let placeholderIndex = 0;
return sql
.replace(/\?/g, () => {
placeholderIndex += 1;
return `$${placeholderIndex}`;
})
.replace(/BEGIN\s+IMMEDIATE\s+TRANSACTION/gi, 'BEGIN')
.replace(/datetime\('now'\)/gi, 'CURRENT_TIMESTAMP')
.replace(/\s+COLLATE\s+NOCASE/gi, '');
};
const openDatabase = async () => {
const pool = new Pool(getPoolConfig());
await pool.query('SELECT 1');
return pool;
};
const closeDatabase = async (db) => {
if (!db || typeof db.end !== 'function') return;
await db.end();
};
const run = async (db, sql, params = []) => {
const result = await db.query(translateSql(sql), params);
return {
lastId: result.rows?.[0]?.id ?? null,
changes: result.rowCount || 0,
rows: result.rows || [],
};
};
const get = async (db, sql, params = []) => {
const result = await db.query(translateSql(sql), params);
return result.rows[0] || null;
};
const all = async (db, sql, params = []) => {
const result = await db.query(translateSql(sql), params);
return result.rows || [];
};
module.exports = {
all,
closeDatabase,
get,
getDefaultDbPath,
openDatabase,
run,
};

View File

@@ -1,86 +1 @@
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const getDefaultDbPath = () => {
return process.env.PLANT_DB_PATH || path.join(__dirname, '..', 'data', 'greenlns.sqlite');
};
const ensureDbDirectory = (dbPath) => {
const directory = path.dirname(dbPath);
fs.mkdirSync(directory, { recursive: true });
};
const openDatabase = (dbPath = getDefaultDbPath()) => {
ensureDbDirectory(dbPath);
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(dbPath, (error) => {
if (error) {
reject(error);
return;
}
resolve(db);
});
});
};
const closeDatabase = (db) => {
return new Promise((resolve, reject) => {
db.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
};
const run = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function onRun(error) {
if (error) {
reject(error);
return;
}
resolve({
lastId: this.lastID,
changes: this.changes,
});
});
});
};
const get = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.get(sql, params, (error, row) => {
if (error) {
reject(error);
return;
}
resolve(row || null);
});
});
};
const all = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.all(sql, params, (error, rows) => {
if (error) {
reject(error);
return;
}
resolve(rows || []);
});
});
};
module.exports = {
all,
closeDatabase,
get,
getDefaultDbPath,
openDatabase,
run,
};
module.exports = require('./postgres');