Files
auth-portal/packages/db/src/index.ts
T
DenozordecandCursor 0b6fa65b08
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 2m30s
Build and Push Auth Portal Docker Image / create-release (push) Skipped
feat(reui): update ReUI components and documentation
- Added new OIDC configuration options in `.env.example`.
- Expanded documentation in `AGENTS.md` to include OIDC endpoints and admin UI.
- Updated ReUI skill version and component count from 17 to 20 across various documentation files.
- Enhanced `README.md` and other related files to reflect the new component structure and usage guidelines.

Co-authored-by: Cursor <[email protected]>
2026-08-11 16:58:47 +07:00

196 lines
6.4 KiB
TypeScript

import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import * as schema from './schema/index.js'
export type Sqlite = Database.Database
export type AppDb = ReturnType<typeof drizzle<typeof schema>>
export function resolveDatabasePath(databaseUrl: string): string {
return databaseUrl.startsWith('sqlite:')
? databaseUrl.slice('sqlite:'.length)
: databaseUrl
}
export function createDb(databaseUrl: string): { db: AppDb; sqlite: Sqlite } {
const path = resolveDatabasePath(databaseUrl)
const sqlite = new Database(path)
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')
sqlite.pragma('synchronous = NORMAL')
const db = drizzle(sqlite, { schema })
return { db, sqlite }
}
export function createMemoryDb(): { db: AppDb; sqlite: Sqlite } {
const sqlite = new Database(':memory:')
sqlite.pragma('foreign_keys = ON')
const db = drizzle(sqlite, { schema })
return { db, sqlite }
}
export function migrateSchema(sqlite: Sqlite): void {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
disabled INTEGER NOT NULL DEFAULT 0,
last_login_at TEXT,
last_login_ip TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_apps (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
app_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_permissions (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS refresh_sessions (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
ip TEXT,
user_agent TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS portal_settings (
id TEXT PRIMARY KEY NOT NULL,
app_switcher_json TEXT,
audit_retention_days INTEGER NOT NULL DEFAULT 90,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY NOT NULL,
event_id TEXT,
source_app TEXT NOT NULL DEFAULT 'portal',
action TEXT NOT NULL,
severity TEXT NOT NULL,
actor_user_id TEXT,
actor_email TEXT,
actor_name TEXT,
target_type TEXT,
target_id TEXT,
summary TEXT NOT NULL,
details_json TEXT,
ip TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_user_apps_user ON user_apps(user_id);
CREATE INDEX IF NOT EXISTS idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_user ON refresh_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_target ON audit_log(target_id, created_at);
`)
// Existing DBs: CREATE TABLE IF NOT EXISTS skips schema — ALTER before indexes on new cols.
const settingsCols = sqlite
.prepare(`PRAGMA table_info(portal_settings)`)
.all() as Array<{ name: string }>
if (!settingsCols.some((c) => c.name === 'audit_retention_days')) {
sqlite.exec(
`ALTER TABLE portal_settings ADD COLUMN audit_retention_days INTEGER NOT NULL DEFAULT 90`,
)
}
const userCols = sqlite
.prepare(`PRAGMA table_info(users)`)
.all() as Array<{ name: string }>
if (!userCols.some((c) => c.name === 'last_login_at')) {
sqlite.exec(`ALTER TABLE users ADD COLUMN last_login_at TEXT`)
}
if (!userCols.some((c) => c.name === 'last_login_ip')) {
sqlite.exec(`ALTER TABLE users ADD COLUMN last_login_ip TEXT`)
}
const sessionCols = sqlite
.prepare(`PRAGMA table_info(refresh_sessions)`)
.all() as Array<{ name: string }>
if (!sessionCols.some((c) => c.name === 'ip')) {
sqlite.exec(`ALTER TABLE refresh_sessions ADD COLUMN ip TEXT`)
}
if (!sessionCols.some((c) => c.name === 'user_agent')) {
sqlite.exec(`ALTER TABLE refresh_sessions ADD COLUMN user_agent TEXT`)
}
const auditCols = sqlite
.prepare(`PRAGMA table_info(audit_log)`)
.all() as Array<{ name: string }>
if (!auditCols.some((c) => c.name === 'event_id')) {
sqlite.exec(`ALTER TABLE audit_log ADD COLUMN event_id TEXT`)
}
if (!auditCols.some((c) => c.name === 'source_app')) {
sqlite.exec(
`ALTER TABLE audit_log ADD COLUMN source_app TEXT NOT NULL DEFAULT 'portal'`,
)
}
sqlite.exec(`
CREATE INDEX IF NOT EXISTS idx_audit_log_source ON audit_log(source_app, created_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_log_event_id ON audit_log(event_id) WHERE event_id IS NOT NULL;
`)
sqlite.exec(`
CREATE TABLE IF NOT EXISTS oidc_clients (
id TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL UNIQUE,
client_secret_hash TEXT NOT NULL,
name TEXT NOT NULL,
redirect_uris_json TEXT NOT NULL,
scopes_json TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oidc_auth_codes (
id TEXT PRIMARY KEY NOT NULL,
code_hash TEXT NOT NULL UNIQUE,
client_id TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
redirect_uri TEXT NOT NULL,
scope TEXT NOT NULL,
nonce TEXT,
code_challenge TEXT,
code_challenge_method TEXT,
expires_at TEXT NOT NULL,
used_at TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oidc_signing_keys (
kid TEXT PRIMARY KEY NOT NULL,
private_pem TEXT NOT NULL,
public_jwk_json TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_client ON oidc_auth_codes(client_id);
CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_user ON oidc_auth_codes(user_id);
`)
}
export function healthCheck(sqlite: Sqlite): void {
sqlite.prepare('SELECT 1').get()
}
export * from './schema/index.js'
export * from './users.js'
export * from './settings.js'
export * from './audit-log.js'
export * from './oidc.js'