Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation. - Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool) - Frontend: Angular 22 SPA (search, checkout, bookings, dashboard) - Database: schema, seed data and demo bookings migrations - Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e), plus a generated evidence report with PDF export - Security: whitebox VAPT report under security/ Secrets (.env, tokens, DB credentials) are git-ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Database chores for TravelHub, driven through psql.
|
||||
*
|
||||
* Credentials come from backend/.env (or the real environment, which wins), so
|
||||
* this works the same on a developer machine and in CI.
|
||||
*
|
||||
* node scripts/db.mjs create create the application database
|
||||
* node scripts/db.mjs migrate apply every migration in order
|
||||
* node scripts/db.mjs reset drop, recreate and migrate (destructive)
|
||||
* node scripts/db.mjs test-db create the empty database the Go tests use
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const MIGRATIONS = join(ROOT, 'backend', 'migrations');
|
||||
|
||||
// --- configuration ---------------------------------------------------------
|
||||
|
||||
/** Reads backend/.env without overriding anything already in the environment. */
|
||||
function loadDotEnv() {
|
||||
const path = join(ROOT, 'backend', '.env');
|
||||
if (!existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq === -1) {
|
||||
continue;
|
||||
}
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (value.length >= 2 && /^["'].*["']$/.test(value)) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (process.env[key] === undefined) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadDotEnv();
|
||||
|
||||
const CONFIG = {
|
||||
host: process.env.DB_HOST ?? 'localhost',
|
||||
port: process.env.DB_PORT ?? '5432',
|
||||
user: process.env.DB_USER ?? 'postgres',
|
||||
password: process.env.DB_PASSWORD ?? '',
|
||||
database: process.env.DB_NAME ?? 'travelhub',
|
||||
testDatabase: process.env.TEST_DB_NAME ?? 'travelhub_test',
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds psql. It is routinely absent from PATH on Windows, so fall back to the
|
||||
* standard install location before giving up.
|
||||
*/
|
||||
function findPsql() {
|
||||
if (process.env.PSQL) {
|
||||
return process.env.PSQL;
|
||||
}
|
||||
try {
|
||||
execFileSync('psql', ['--version'], { stdio: 'ignore' });
|
||||
return 'psql';
|
||||
} catch {
|
||||
// Not on PATH; try the usual Windows install root, newest version first.
|
||||
const base = 'C:\\Program Files\\PostgreSQL';
|
||||
if (existsSync(base)) {
|
||||
const versions = readdirSync(base).sort((a, b) => Number(b) - Number(a));
|
||||
for (const version of versions) {
|
||||
const candidate = join(base, version, 'bin', 'psql.exe');
|
||||
if (existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
'psql was not found. Add it to PATH, or set PSQL to its full path.\n' +
|
||||
'On Windows it usually lives in C:\\Program Files\\PostgreSQL\\17\\bin',
|
||||
);
|
||||
}
|
||||
|
||||
const PSQL = findPsql();
|
||||
|
||||
// --- psql helpers ----------------------------------------------------------
|
||||
|
||||
function run(args, { database = 'postgres', capture = false } = {}) {
|
||||
return execFileSync(
|
||||
PSQL,
|
||||
['-U', CONFIG.user, '-h', CONFIG.host, '-p', CONFIG.port, '-d', database, ...args],
|
||||
{
|
||||
env: { ...process.env, PGPASSWORD: CONFIG.password },
|
||||
encoding: 'utf8',
|
||||
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function databaseExists(name) {
|
||||
const out = run(['-tAc', `SELECT 1 FROM pg_database WHERE datname='${name}'`], { capture: true });
|
||||
return out.trim() === '1';
|
||||
}
|
||||
|
||||
function createDatabase(name) {
|
||||
if (databaseExists(name)) {
|
||||
console.log(`· ${name} already exists`);
|
||||
return false;
|
||||
}
|
||||
run(['-c', `CREATE DATABASE "${name}"`], { capture: true });
|
||||
console.log(`✓ created ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function dropDatabase(name) {
|
||||
if (!databaseExists(name)) {
|
||||
return;
|
||||
}
|
||||
// Existing sessions would block the drop, so close them first.
|
||||
run(
|
||||
[
|
||||
'-c',
|
||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||
WHERE datname='${name}' AND pid <> pg_backend_pid()`,
|
||||
],
|
||||
{ capture: true },
|
||||
);
|
||||
run(['-c', `DROP DATABASE "${name}"`], { capture: true });
|
||||
console.log(`✓ dropped ${name}`);
|
||||
}
|
||||
|
||||
function migrate(name) {
|
||||
const files = readdirSync(MIGRATIONS)
|
||||
.filter((f) => f.endsWith('.sql'))
|
||||
.sort();
|
||||
|
||||
for (const file of files) {
|
||||
process.stdout.write(` applying ${file} ... `);
|
||||
run(['-v', 'ON_ERROR_STOP=1', '-q', '-f', join(MIGRATIONS, file)], { database: name });
|
||||
console.log('ok');
|
||||
}
|
||||
console.log(`✓ ${name} is up to date (${files.length} migrations)`);
|
||||
}
|
||||
|
||||
// --- commands --------------------------------------------------------------
|
||||
|
||||
const commands = {
|
||||
create() {
|
||||
createDatabase(CONFIG.database);
|
||||
},
|
||||
|
||||
migrate() {
|
||||
createDatabase(CONFIG.database);
|
||||
migrate(CONFIG.database);
|
||||
},
|
||||
|
||||
reset() {
|
||||
console.log(`Resetting ${CONFIG.database}. All data in it will be lost.`);
|
||||
dropDatabase(CONFIG.database);
|
||||
createDatabase(CONFIG.database);
|
||||
migrate(CONFIG.database);
|
||||
},
|
||||
|
||||
'test-db'() {
|
||||
// The Go tests apply the schema themselves; they only need the database.
|
||||
createDatabase(CONFIG.testDatabase);
|
||||
console.log(`✓ ${CONFIG.testDatabase} ready for go test ./...`);
|
||||
},
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
if (!command || !(command in commands)) {
|
||||
console.error(`Usage: node scripts/db.mjs <${Object.keys(commands).join('|')}>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!CONFIG.password) {
|
||||
console.error('No database password. Set DB_PASSWORD, or fill in backend/.env.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
commands[command]();
|
||||
} catch (err) {
|
||||
console.error(`\n✗ ${command} failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user