Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
318 lines
11 KiB
JavaScript
318 lines
11 KiB
JavaScript
import pg from 'pg';
|
|
import dotenv from 'dotenv';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname } from 'path';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { getSecretProviderInstance } from '../../../shared/core/index.js';
|
|
|
|
// Enable long stack traces for async operations
|
|
Error.stackTraceLimit = 50;
|
|
process.env.NODE_OPTIONS = '--async-stack-traces';
|
|
|
|
// Calculate secrets directory path once at module load
|
|
const DOCKER_SECRETS_PATH = '/run/secrets';
|
|
const LOCAL_SECRETS_PATH = '../secrets';
|
|
const SECRETS_PATH = fs.existsSync(DOCKER_SECRETS_PATH) ? DOCKER_SECRETS_PATH : LOCAL_SECRETS_PATH;
|
|
|
|
async function getSecret(secretName, envVar, defaultValue = '') {
|
|
try {
|
|
const secretProvider = await getSecretProviderInstance();
|
|
const secret = await secretProvider.getAppSecret(secretName);
|
|
if (secret) {
|
|
console.log(`Successfully read secret '${secretName}' from secret provider`);
|
|
return secret;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to read secret '${secretName}' from secret provider:`, error.message);
|
|
}
|
|
|
|
// Fallback to environment variable
|
|
if (process.env[envVar]) {
|
|
console.warn(`Using ${envVar} environment variable instead of secret provider`);
|
|
return process.env[envVar] || defaultValue;
|
|
}
|
|
|
|
console.warn(`Neither secret provider nor ${envVar} environment variable found, using default value`);
|
|
return defaultValue;
|
|
}
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const { Client } = pg;
|
|
|
|
dotenv.config();
|
|
|
|
const MAX_RETRIES = 5;
|
|
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
|
|
|
/**
|
|
* Configuration validation
|
|
* @throws {Error} If required environment variables are missing
|
|
*/
|
|
function validateConfig() {
|
|
const required = [
|
|
'DB_HOST',
|
|
'DB_PORT',
|
|
'DB_NAME_SERVER',
|
|
'DB_USER_SERVER',
|
|
'APP_ENV'
|
|
];
|
|
|
|
const missing = required.filter(key => !process.env[key]);
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to set up the hocuspocus database
|
|
* This is a non-fatal operation - if it fails, we log the error but continue
|
|
*/
|
|
async function setupHocuspocusDatabase(client, postgresPassword) {
|
|
// Default to 'hocuspocus' if environment variables are not set
|
|
process.env.DB_NAME_HOCUSPOCUS = process.env.DB_NAME_HOCUSPOCUS || 'hocuspocus';
|
|
process.env.DB_USER_HOCUSPOCUS = process.env.DB_USER_HOCUSPOCUS || 'hocuspocus_user';
|
|
|
|
// Get hocuspocus password from secrets or env var
|
|
const hocuspocusPassword = await getSecret('db_password_hocuspocus', 'DB_PASSWORD_HOCUSPOCUS', postgresPassword);
|
|
|
|
try {
|
|
// Check if database exists
|
|
const dbCheckResult = await client.query(
|
|
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
[process.env.DB_NAME_HOCUSPOCUS]
|
|
);
|
|
|
|
if (dbCheckResult.rows.length > 0) {
|
|
console.log(`Database ${process.env.DB_NAME_HOCUSPOCUS} already exists`);
|
|
} else {
|
|
await client.query(`CREATE DATABASE ${process.env.DB_NAME_HOCUSPOCUS}`);
|
|
console.log(`Database ${process.env.DB_NAME_HOCUSPOCUS} created successfully`);
|
|
}
|
|
|
|
// Connect to the hocuspocus database
|
|
const hocuspocusClient = new Client({
|
|
host: process.env.DB_HOST,
|
|
port: process.env.DB_PORT,
|
|
user: 'postgres',
|
|
password: postgresPassword,
|
|
database: process.env.DB_NAME_HOCUSPOCUS
|
|
});
|
|
|
|
await hocuspocusClient.connect();
|
|
|
|
// Check if hocuspocus user exists
|
|
const userCheckResult = await hocuspocusClient.query(
|
|
"SELECT 1 FROM pg_roles WHERE rolname = $1",
|
|
[process.env.DB_USER_HOCUSPOCUS]
|
|
);
|
|
|
|
if (userCheckResult.rows.length > 0) {
|
|
console.log(`User ${process.env.DB_USER_HOCUSPOCUS} already exists`);
|
|
// Update password for existing user
|
|
await hocuspocusClient.query(`ALTER USER ${process.env.DB_USER_HOCUSPOCUS} WITH PASSWORD '${hocuspocusPassword}'`);
|
|
console.log(`Updated password for user ${process.env.DB_USER_HOCUSPOCUS}`);
|
|
} else {
|
|
await hocuspocusClient.query(`CREATE USER ${process.env.DB_USER_HOCUSPOCUS} WITH PASSWORD '${hocuspocusPassword}'`);
|
|
console.log(`User ${process.env.DB_USER_HOCUSPOCUS} created successfully`);
|
|
}
|
|
|
|
// Grant necessary permissions
|
|
await hocuspocusClient.query(`GRANT ALL PRIVILEGES ON DATABASE ${process.env.DB_NAME_HOCUSPOCUS} TO ${process.env.DB_USER_HOCUSPOCUS}`);
|
|
await hocuspocusClient.query(`GRANT ALL PRIVILEGES ON SCHEMA public TO ${process.env.DB_USER_HOCUSPOCUS}`);
|
|
|
|
await hocuspocusClient.end();
|
|
console.log('Hocuspocus database setup completed successfully');
|
|
} catch (error) {
|
|
console.warn('Warning: Hocuspocus database setup failed:', error.message);
|
|
console.log('Continuing with main application setup...');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates database and users with appropriate permissions and RLS policies
|
|
* @param {number} retryCount - Number of retry attempts
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function createDatabase(retryCount = 0) {
|
|
try {
|
|
validateConfig();
|
|
} catch (error) {
|
|
console.error('Configuration validation failed:', error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Read passwords from secret files
|
|
const postgresPassword = await getSecret('postgres_password', 'DB_PASSWORD_ADMIN');
|
|
if (!postgresPassword) {
|
|
console.error('Error: No postgres password available');
|
|
process.exit(1);
|
|
}
|
|
|
|
const serverPassword = await getSecret('db_password_server', 'DB_PASSWORD_SERVER');
|
|
if (!serverPassword) {
|
|
console.error('Error: No server password available');
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = new Client({
|
|
host: process.env.DB_HOST,
|
|
port: process.env.DB_PORT,
|
|
user: 'postgres',
|
|
password: postgresPassword,
|
|
database: 'postgres' // Connect to default postgres database
|
|
});
|
|
|
|
try {
|
|
await client.connect();
|
|
console.log('Connected to PostgreSQL server');
|
|
|
|
// Try to set up hocuspocus database (non-fatal if it fails)
|
|
await setupHocuspocusDatabase(client, postgresPassword);
|
|
|
|
// Check if database exists
|
|
const dbCheckResult = await client.query(
|
|
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
[process.env.DB_NAME_SERVER]
|
|
);
|
|
|
|
if (dbCheckResult.rows.length > 0) {
|
|
console.log(`Database ${process.env.DB_NAME_SERVER} already exists`);
|
|
} else {
|
|
await client.query(`CREATE DATABASE ${process.env.DB_NAME_SERVER}`);
|
|
console.log(`Database ${process.env.DB_NAME_SERVER} created successfully`);
|
|
}
|
|
|
|
// Close connection to postgres database
|
|
await client.end();
|
|
|
|
// Connect to the newly created database
|
|
const dbClient = new Client({
|
|
host: process.env.DB_HOST,
|
|
port: process.env.DB_PORT,
|
|
user: 'postgres',
|
|
password: postgresPassword,
|
|
database: process.env.DB_NAME_SERVER
|
|
});
|
|
|
|
await dbClient.connect();
|
|
|
|
// Create extensions required for EE
|
|
// Check if database is already fully configured by checking for tenants table
|
|
try {
|
|
const tenantsCheck = await dbClient.query("SELECT 1 FROM information_schema.tables WHERE table_name = 'tenants'");
|
|
if (tenantsCheck.rows.length > 0) {
|
|
console.log('Database appears to be already configured (tenants table exists). Skipping setup.');
|
|
await dbClient.end();
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log('Tenants table not found, proceeding with database setup...');
|
|
}
|
|
|
|
// Set Citus mode to sequential for DDL operations if Citus is available
|
|
try {
|
|
await dbClient.query(`SET LOCAL citus.multi_shard_modify_mode TO 'sequential';`);
|
|
console.log('Citus sequential mode enabled');
|
|
} catch (error) {
|
|
// Ignore error if Citus is not installed
|
|
console.log('Citus not detected, continuing with standard PostgreSQL');
|
|
}
|
|
await dbClient.query(`
|
|
CREATE EXTENSION IF NOT EXISTS "vector";
|
|
`);
|
|
|
|
console.log('Database extensions created successfully');
|
|
|
|
// Check if app_user exists
|
|
const userCheckResult = await dbClient.query(
|
|
"SELECT 1 FROM pg_roles WHERE rolname = $1",
|
|
[process.env.DB_USER_SERVER]
|
|
);
|
|
|
|
if (userCheckResult.rows.length > 0) {
|
|
console.log(`User ${process.env.DB_USER_SERVER} already exists`);
|
|
// Update password for existing user
|
|
await dbClient.query(`ALTER USER ${process.env.DB_USER_SERVER} WITH PASSWORD '${serverPassword}'`);
|
|
console.log(`Updated password for user ${process.env.DB_USER_SERVER}`);
|
|
} else {
|
|
await dbClient.query(`CREATE USER ${process.env.DB_USER_SERVER} WITH PASSWORD '${serverPassword}'`);
|
|
console.log(`User ${process.env.DB_USER_SERVER} created successfully`);
|
|
}
|
|
|
|
// Configure database
|
|
await dbClient.query(`ALTER DATABASE ${process.env.DB_NAME_SERVER} SET app.environment = '${process.env.APP_ENV}'`);
|
|
|
|
// Ensure postgres user has necessary permissions
|
|
await dbClient.query(`GRANT ALL PRIVILEGES ON DATABASE ${process.env.DB_NAME_SERVER} TO postgres`);
|
|
await dbClient.query(`GRANT ALL PRIVILEGES ON SCHEMA public TO postgres`);
|
|
await dbClient.query(`ALTER USER postgres WITH CREATEDB CREATEROLE`);
|
|
|
|
// Grant CREATE permission on public schema to postgres user
|
|
await dbClient.query(`ALTER SCHEMA public OWNER TO postgres`);
|
|
await dbClient.query(`GRANT CREATE ON SCHEMA public TO postgres`);
|
|
|
|
// Set up RLS and permissions
|
|
console.log('Setting up Row Level Security...');
|
|
|
|
// Grant connect permission
|
|
await dbClient.query(`GRANT CONNECT ON DATABASE ${process.env.DB_NAME_SERVER} TO ${process.env.DB_USER_SERVER}`);
|
|
|
|
// Grant usage on schema
|
|
await dbClient.query(`GRANT USAGE ON SCHEMA public TO ${process.env.DB_USER_SERVER}`);
|
|
|
|
// Grant basic table permissions (but not ALL)
|
|
await dbClient.query(`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${process.env.DB_USER_SERVER}`);
|
|
await dbClient.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${process.env.DB_USER_SERVER}`);
|
|
|
|
// Grant sequence permissions
|
|
await dbClient.query(`GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO ${process.env.DB_USER_SERVER}`);
|
|
await dbClient.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO ${process.env.DB_USER_SERVER}`);
|
|
|
|
// Enable RLS on all tables
|
|
await dbClient.query(`
|
|
DO $$
|
|
DECLARE
|
|
r RECORD;
|
|
BEGIN
|
|
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
|
|
LOOP
|
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', r.tablename);
|
|
END LOOP;
|
|
END $$;
|
|
`);
|
|
|
|
// Create tenant_id function for RLS
|
|
await dbClient.query(`
|
|
CREATE OR REPLACE FUNCTION current_tenant_id()
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_tenant', TRUE);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
`);
|
|
|
|
console.log('Enterprise database setup completed successfully');
|
|
await dbClient.end();
|
|
} catch (error) {
|
|
console.error('Error during database setup:', error);
|
|
|
|
if (retryCount < MAX_RETRIES) {
|
|
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
|
|
console.log(`Retrying in ${delay / 1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
return createDatabase(retryCount + 1);
|
|
}
|
|
|
|
console.error(`Max retries (${MAX_RETRIES}) reached. Database setup failed.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Execute database setup
|
|
createDatabase().catch(error => {
|
|
console.error('Unhandled error during database setup:', error);
|
|
process.exit(1);
|
|
});
|