- 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]>
196 lines
6.4 KiB
TypeScript
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'
|