- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration - Frontend: React 18 + Vite + TailwindCSS/DaisyUI - Security fixes applied (2026-07 + 2026-08): - LDAP injection prevention, CSRF protection, HttpOnly cookies - Session hashing (SHA-256), account lockout, rate limiting - Input validation (zod), file upload security, CSP/HSTS headers - V3: express-rate-limit updated (ip-address SSRF fix) - V4: postcss updated (nanoid DoS fix) - V5: Rate-limit on /health endpoint - V6: Session rotation on login (session fixation prevention) - V9: Task values array limit (DoS prevention) - V10: Frontend XSS audit completed - Docker: Multi-stage build, non-root user, PostgreSQL + backup service
154 lines
4.4 KiB
JavaScript
154 lines
4.4 KiB
JavaScript
/**
|
|
* Database abstraction layer (async).
|
|
*
|
|
* Punkt 4: PostgreSQL for production, SQLite fallback for development.
|
|
* Both modes expose the SAME async API: db.prepare(sql).run/get/all() return Promises.
|
|
*
|
|
* - If DATABASE_URL starts with 'postgresql://' → PostgreSQL (pg)
|
|
* - Otherwise → SQLite (better-sqlite3, wrapped in Promises for unified async API)
|
|
*/
|
|
const path = require('path');
|
|
|
|
const DATABASE_URL = process.env.DATABASE_URL || '';
|
|
const usePostgres = DATABASE_URL.startsWith('postgresql://') || DATABASE_URL.startsWith('postgres://');
|
|
|
|
// Helper: convert SQLite ? placeholders to PostgreSQL $1, $2, etc.
|
|
function convertPlaceholders(sql) {
|
|
let idx = 0;
|
|
return sql.replace(/\?/g, () => { idx++; return '$' + idx; });
|
|
}
|
|
|
|
let db;
|
|
|
|
if (usePostgres) {
|
|
// ============ PostgreSQL mode (production) ============
|
|
const { Pool } = require('pg');
|
|
const pool = new Pool({
|
|
connectionString: DATABASE_URL,
|
|
max: 10,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 10000,
|
|
});
|
|
|
|
pool.on('error', (err) => {
|
|
console.error('[DB] PostgreSQL Pool-Fehler:', err.message);
|
|
});
|
|
|
|
console.log('[DB] PostgreSQL-Verbindung hergestellt (Production-Modus).');
|
|
|
|
db = {
|
|
_pool: pool,
|
|
_type: 'postgres',
|
|
|
|
prepare(sql) {
|
|
const pgSql = convertPlaceholders(sql);
|
|
return {
|
|
run: (...params) => {
|
|
// For INSERT statements, append RETURNING id to get the generated ID
|
|
const isInsert = pgSql.trim().toUpperCase().startsWith('INSERT');
|
|
const finalSql = isInsert && !pgSql.toUpperCase().includes('RETURNING')
|
|
? pgSql.replace(/;?\s*$/, ' RETURNING id')
|
|
: pgSql;
|
|
return pool.query(finalSql, params).then(result => ({
|
|
changes: result.rowCount,
|
|
lastInsertRowid: result.rows[0]?.id || null,
|
|
}));
|
|
},
|
|
get: (...params) => pool.query(pgSql, params).then(result => result.rows[0] || null),
|
|
all: (...params) => pool.query(pgSql, params).then(result => result.rows),
|
|
};
|
|
},
|
|
|
|
exec(sql) {
|
|
return pool.query(sql);
|
|
},
|
|
|
|
pragma(_str) {
|
|
return Promise.resolve({});
|
|
},
|
|
|
|
transaction(fn) {
|
|
return async (...args) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const txDb = {
|
|
prepare(sql) {
|
|
const pgSql = convertPlaceholders(sql);
|
|
return {
|
|
run: (...params) => client.query(pgSql, params).then(result => ({
|
|
changes: result.rowCount,
|
|
lastInsertRowid: result.rows[0]?.id || null,
|
|
})),
|
|
get: (...params) => client.query(pgSql, params).then(result => result.rows[0] || null),
|
|
all: (...params) => client.query(pgSql, params).then(result => result.rows),
|
|
};
|
|
},
|
|
exec: (sql) => client.query(sql),
|
|
pragma: () => Promise.resolve({}),
|
|
};
|
|
|
|
const result = await fn.call(txDb, ...args);
|
|
await client.query('COMMIT');
|
|
return result;
|
|
} catch (err) {
|
|
await client.query('ROLLBACK');
|
|
throw err;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
},
|
|
|
|
close() {
|
|
return pool.end();
|
|
},
|
|
};
|
|
} else {
|
|
// ============ SQLite mode (development) ============
|
|
// Wrapped in Promises so the API is identical to PostgreSQL (async)
|
|
const Database = require('better-sqlite3');
|
|
const dbPath = path.join(__dirname, 'data', 'workflow.db');
|
|
const sqliteDb = new Database(dbPath);
|
|
|
|
sqliteDb.pragma('journal_mode = WAL');
|
|
sqliteDb.pragma('foreign_keys = ON');
|
|
|
|
console.log('[DB] SQLite-Datenbank verbunden (better-sqlite3, WAL-Modus, async-Wrapper).');
|
|
|
|
db = {
|
|
_type: 'sqlite',
|
|
|
|
prepare(sql) {
|
|
const stmt = sqliteDb.prepare(sql);
|
|
return {
|
|
run: (...params) => Promise.resolve(stmt.run(...params)),
|
|
get: (...params) => Promise.resolve(stmt.get(...params)),
|
|
all: (...params) => Promise.resolve(stmt.all(...params)),
|
|
};
|
|
},
|
|
|
|
exec(sql) {
|
|
sqliteDb.exec(sql);
|
|
return Promise.resolve();
|
|
},
|
|
|
|
pragma(str) {
|
|
sqliteDb.pragma(str);
|
|
return Promise.resolve({});
|
|
},
|
|
|
|
transaction(fn) {
|
|
const tx = sqliteDb.transaction(fn);
|
|
return (...args) => Promise.resolve(tx(...args));
|
|
},
|
|
|
|
close() {
|
|
sqliteDb.close();
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = db; |