fix(platform): apply production hardening from audit plan

Close billing, tenancy, migration, build, and CI/CD gaps identified in the
audit: wallet/gateway guards, full-UUID namespaces, idempotent migrations with
base schema, stateful service stability, safer Dockerfiles/git builds, and
platform chart hardening (Redis auth, RollingUpdate, backups, Swagger off).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-07-02 19:35:07 +03:30
parent 34c110be6a
commit 22359be40e
55 changed files with 4883 additions and 381 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Generate the greenfield base schema (000_base_schema.sql) by letting TypeORM
* `synchronize` build every table from the entities against a throwaway
* Postgres, then dumping the schema. Run when entities change materially:
*
* docker run -d --name ch-schemagen -e POSTGRES_PASSWORD=pass \
* -e POSTGRES_USER=cloudhost -e POSTGRES_DB=cloudhost \
* -p 55432:5432 postgres:16-alpine
* node scripts/generate-base-schema.mjs
*
* The output is wrapped so it is safe to run on an already-populated database
* (every statement uses IF NOT EXISTS / duplicate_object guards where possible;
* the migration runner also records it in schema_migrations so it runs once).
*/
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const backendRoot = path.resolve(__dirname, '..');
const outPath = path.join(backendRoot, 'migrations', '000_base_schema.sql');
// Use the COMPILED entities (run `npm run build` first) — union-typed columns
// only carry correct decorator metadata through the project's tsc build.
const ds = new DataSource({
type: 'postgres',
host: process.env.SCHEMA_DB_HOST || '127.0.0.1',
port: parseInt(process.env.SCHEMA_DB_PORT || '55432', 10),
username: 'cloudhost',
password: 'pass',
database: 'cloudhost',
entities: [path.join(backendRoot, 'dist/**/*.entity.js')],
synchronize: true,
});
await ds.initialize();
await ds.destroy();
// Dump schema-only from the container, then strip owner/ACL noise.
const dumped = execFileSync('docker', [
'exec', 'ch-schemagen',
'pg_dump', '-U', 'cloudhost', '-d', 'cloudhost',
'--schema-only', '--no-owner', '--no-privileges',
], { maxBuffer: 32 * 1024 * 1024 }).toString();
// Strip:
// - psql client meta-commands that are version-specific (\restrict is
// pg_dump 16.13+ only) and would break on the migrations image's psql;
// - the `search_path = ''` reset, which otherwise persists into the trailing
// `INSERT INTO schema_migrations` the runner appends (unqualified) and the
// footer below, causing "no schema has been selected to create in".
const raw = dumped
.split('\n')
.filter(
(line) =>
!/^\\(restrict|unrestrict)\b/.test(line) &&
!/set_config\('search_path'/.test(line),
)
.join('\n');
const header = `-- 000_base_schema.sql — greenfield base schema (generated from TypeORM entities).
-- Auto-generated by scripts/generate-base-schema.mjs. Do not edit by hand.
-- Incremental migrations (001+) run afterwards on top of this schema.
`;
// The legacy pricing-catalog migrations (004-009) target a superseded
// snake_case pricing schema that is incompatible with the current entities.
// On greenfield the base schema already creates the entity-shaped pricing
// tables and the app self-seeds their rows (PricingCatalogService.ensureDefaults
// on boot), so mark those migrations as already applied to skip them.
const supersededPricingMigrations = [
'004_pricing_catalog.sql',
'005_pricing_catalog_all_runtimes.sql',
'006_addon_rate_resources.sql',
'007_optional_service_pricing_matrix.sql',
'008_application_optional_service_resources.sql',
'009_optional_service_deploy_defaults.sql',
];
const footer = `
-- Mark superseded legacy pricing migrations as applied (see generator note).
CREATE TABLE IF NOT EXISTS schema_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
INSERT INTO schema_migrations (filename) VALUES
${supersededPricingMigrations.map((m) => ` ('${m}')`).join(',\n')}
ON CONFLICT (filename) DO NOTHING;
`;
fs.writeFileSync(outPath, header + raw + footer);
console.log(`Wrote ${outPath} (${raw.length} bytes)`);