feat(database): refactor CLI, add column whitelist, and SSL config

- Add `ZEN_DB_SSL_DISABLED` env variable to allow disabling SSL for database connections
- Refactor database CLI to split init logic into `initFeatures` and `initModules` for modular table initialization, with graceful fallback when modules are absent
- Extract `printHelp` and `askConfirmation` helpers for cleaner CLI structure
- Ensure `closePool` is called on both success and error paths in CLI
- Add `filterAllowedColumns` utility in `crud.js` to enforce column whitelists, preventing mass-assignment of privileged fields (e.g. `role`, `email_verified`)
- Update drop command description from "auth tables" to "all tables"
This commit is contained in:
2026-04-13 16:35:23 -04:00
parent 6521179e10
commit a3921a0b98
11 changed files with 691 additions and 295 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* Auth Feature - Database
* Creates and drops zen_auth_* tables.
*/
import { query, tableExists } from '@zen/core/database';
import { done, warn } from '../../shared/lib/logger.js';
const AUTH_TABLES = [
{
name: 'zen_auth_users',
sql: `
CREATE TABLE zen_auth_users (
id text NOT NULL PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE,
email_verified boolean NOT NULL DEFAULT false,
image text,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
role text DEFAULT 'user' CHECK (role IN ('admin', 'user'))
)
`
},
{
name: 'zen_auth_sessions',
sql: `
CREATE TABLE zen_auth_sessions (
id text NOT NULL PRIMARY KEY,
expires_at timestamptz NOT NULL,
token text NOT NULL UNIQUE,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at timestamptz NOT NULL,
ip_address text,
user_agent text,
user_id text NOT NULL REFERENCES zen_auth_users (id) ON DELETE CASCADE
)
`
},
{
name: 'zen_auth_accounts',
sql: `
CREATE TABLE zen_auth_accounts (
id text NOT NULL PRIMARY KEY,
account_id text NOT NULL,
provider_id text NOT NULL,
user_id text NOT NULL REFERENCES zen_auth_users (id) ON DELETE CASCADE,
access_token text,
refresh_token text,
id_token text,
access_token_expires_at timestamptz,
refresh_token_expires_at timestamptz,
scope text,
password text,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at timestamptz NOT NULL
)
`
},
{
name: 'zen_auth_verifications',
sql: `
CREATE TABLE zen_auth_verifications (
id text NOT NULL PRIMARY KEY,
identifier text NOT NULL,
value text NOT NULL,
token text NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL
)
`
}
];
/**
* Create all authentication tables.
* @returns {Promise<{ created: string[], skipped: string[] }>}
*/
export async function createTables() {
const created = [];
const skipped = [];
for (const table of AUTH_TABLES) {
const exists = await tableExists(table.name);
if (!exists) {
await query(table.sql);
created.push(table.name);
done(`Created table: ${table.name}`);
} else {
skipped.push(table.name);
}
}
return { created, skipped };
}
/**
* Drop all authentication tables in reverse dependency order.
* @returns {Promise<void>}
*/
export async function dropTables() {
const dropOrder = [...AUTH_TABLES].reverse().map(t => t.name);
warn('Dropping all Zen authentication tables...');
for (const tableName of dropOrder) {
const exists = await tableExists(tableName);
if (exists) {
await query(`DROP TABLE IF EXISTS "${tableName}" CASCADE`);
done(`Dropped table: ${tableName}`);
}
}
done('All authentication tables dropped');
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Core Features Registry
*
* Lists all built-in features that are always initialized when running `zen-db init`.
* Unlike optional modules (src/modules), core features are not gated by env vars —
* they are required for the application to function.
*
* Each name must correspond to a directory under src/features/ that exposes a db.js
* with createTables() and optionally dropTables().
*/
export const CORE_FEATURES = [
'auth',
];
+65
View File
@@ -0,0 +1,65 @@
/**
* Core Feature Database Initialization (CLI)
*
* Initializes and drops DB tables for each core feature.
* Features are discovered from CORE_FEATURES — no manual wiring needed
* when adding a new feature.
*/
import { CORE_FEATURES } from './features.registry.js';
import { done, fail, info, step } from '../shared/lib/logger.js';
/**
* Initialize all core feature databases.
* @returns {Promise<{ created: string[], skipped: string[] }>}
*/
export async function initFeatures() {
const created = [];
const skipped = [];
step('Initializing feature databases...');
for (const featureName of CORE_FEATURES) {
try {
step(`Initializing ${featureName}...`);
const db = await import(`./${featureName}/db.js`);
if (typeof db.createTables === 'function') {
const result = await db.createTables();
if (result?.created) created.push(...result.created);
if (result?.skipped) skipped.push(...result.skipped);
done(`${featureName} initialized`);
} else {
info(`${featureName} has no createTables function`);
}
} catch (error) {
fail(`${featureName}: ${error.message}`);
throw error;
}
}
return { created, skipped };
}
/**
* Drop all core feature databases in reverse order.
* @returns {Promise<void>}
*/
export async function dropFeatures() {
for (const featureName of [...CORE_FEATURES].reverse()) {
try {
const db = await import(`./${featureName}/db.js`);
if (typeof db.dropTables === 'function') {
await db.dropTables();
} else {
info(`${featureName} has no dropTables function`);
}
} catch (error) {
fail(`${featureName}: ${error.message}`);
throw error;
}
}
}