90 lines
3.5 KiB
JavaScript
90 lines
3.5 KiB
JavaScript
import { connect } from '@tursodatabase/database';
|
|
import { access, mkdir, readFile } from 'node:fs/promises';
|
|
import { dirname, resolve } from 'node:path';
|
|
import process from 'node:process';
|
|
import { initializeDefaultUsers } from './default-users.mjs';
|
|
|
|
const databasePath = resolve(process.env.TDR_DATABASE_PATH || './data/shd-build.db');
|
|
const schemaPath = resolve('./database/schema.sql');
|
|
let databaseAlreadyExisted = true;
|
|
try {
|
|
await access(databasePath);
|
|
} catch {
|
|
databaseAlreadyExisted = false;
|
|
}
|
|
|
|
await mkdir(dirname(databasePath), { recursive: true });
|
|
|
|
const database = await connect(databasePath);
|
|
const schema = await readFile(schemaPath, 'utf8');
|
|
|
|
const usersTable = await (await database.prepare(
|
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'"
|
|
)).get();
|
|
if (usersTable) {
|
|
const userColumns = await (await database.prepare("PRAGMA table_info('users')")).all();
|
|
if (!userColumns.some((column) => column.name === 'login_username')) {
|
|
console.log('Separating optional local login usernames from internal account identifiers.');
|
|
await database.exec('ALTER TABLE users ADD COLUMN login_username TEXT');
|
|
await database.exec('UPDATE users SET login_username=username WHERE password_hash IS NOT NULL');
|
|
}
|
|
}
|
|
|
|
// The project schema deliberately avoids semicolons inside string literals.
|
|
const statements = schema
|
|
.split(';')
|
|
.map((statement) => statement.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const statement of statements) {
|
|
await database.exec(statement);
|
|
}
|
|
|
|
const loginEventsDefinition = await (await database.prepare(
|
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='login_events'"
|
|
)).get();
|
|
if (typeof loginEventsDefinition?.sql === 'string' && !loginEventsDefinition.sql.includes('magic_link')) {
|
|
console.log('Expanding login event providers for passwordless authentication.');
|
|
await database.exec('ALTER TABLE login_events RENAME TO login_events_legacy');
|
|
await database.exec(`CREATE TABLE login_events (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
provider TEXT NOT NULL CHECK(provider IN ('password', 'discord', 'magic_link', 'passkey')),
|
|
succeeded INTEGER NOT NULL CHECK(succeeded IN (0, 1)),
|
|
created_at TEXT NOT NULL
|
|
)`);
|
|
await database.exec(`INSERT INTO login_events(id,user_id,provider,succeeded,created_at)
|
|
SELECT id,user_id,provider,succeeded,created_at FROM login_events_legacy`);
|
|
await database.exec('DROP TABLE login_events_legacy');
|
|
await database.exec('CREATE INDEX IF NOT EXISTS login_events_user_created_idx ON login_events(user_id, created_at DESC)');
|
|
}
|
|
const loginEventColumns = await (await database.prepare("PRAGMA table_info('login_events')")).all();
|
|
for (const [name, definition] of [
|
|
['ip_address', 'TEXT'],
|
|
['user_agent', 'TEXT'],
|
|
['client_name', 'TEXT'],
|
|
['operating_system', 'TEXT'],
|
|
['device_type', 'TEXT'],
|
|
['location_country', 'TEXT'],
|
|
['location_region', 'TEXT'],
|
|
['location_city', 'TEXT'],
|
|
['failure_reason', 'TEXT'],
|
|
['risk_flags', 'TEXT'],
|
|
['credential_id', 'TEXT'],
|
|
['session_id', 'TEXT REFERENCES sessions(id) ON DELETE SET NULL'],
|
|
['session_ended_at', 'TEXT'],
|
|
['session_end_reason', 'TEXT'],
|
|
['sensitive_purged_at', 'TEXT']
|
|
]) {
|
|
if (!loginEventColumns.some((column) => column.name === name)) {
|
|
await database.exec(`ALTER TABLE login_events ADD COLUMN ${name} ${definition}`);
|
|
}
|
|
}
|
|
|
|
console.log(`Applied ${statements.length} schema statements to ${databasePath}`);
|
|
|
|
if (!databaseAlreadyExisted) {
|
|
console.log('New database detected; creating default users.');
|
|
await initializeDefaultUsers(database);
|
|
}
|