Files
Webserver/lib/db.ts

103 lines
2.9 KiB
TypeScript
Raw Normal View History

import Database from 'better-sqlite3';
import type BetterSqlite3 from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
2026-05-25 09:49:40 -04:00
import crypto from 'crypto';
let db: BetterSqlite3.Database | null = null;
let insertStmt: BetterSqlite3.Statement | null = null;
2026-05-25 09:49:40 -04:00
const runtimeAnalyticsSalt = crypto.randomBytes(32).toString('hex');
function getAnalyticsSalt() {
return process.env.ANALYTICS_SALT || runtimeAnalyticsSalt;
}
function getDbPath() {
if (process.env.VISIT_DB_PATH) {
return process.env.VISIT_DB_PATH;
}
const serverStorage = '/server_storage';
try {
fs.mkdirSync(serverStorage, { recursive: true, mode: 0o700 });
fs.accessSync(serverStorage, fs.constants.W_OK);
return path.join(serverStorage, 'visitors.db');
} catch {
const localStorage = path.join(process.cwd(), '.data');
fs.mkdirSync(localStorage, { recursive: true, mode: 0o700 });
return path.join(localStorage, 'visitors.db');
}
}
function createVisitorId(clientAddress: string, userAgent: string) {
const day = new Date().toISOString().slice(0, 10);
return crypto
.createHmac('sha256', getAnalyticsSalt())
.update(`${day}:${clientAddress}:${userAgent}`)
.digest('hex')
.slice(0, 32);
}
function ensureSchema(database: BetterSqlite3.Database) {
database.exec(`
CREATE TABLE IF NOT EXISTS visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
visitor_id TEXT NOT NULL,
path TEXT NOT NULL,
visited_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
const columns = database
.prepare("PRAGMA table_info('visits')")
.all() as { name: string; notnull: number }[];
const columnNames = new Set(columns.map((column) => column.name));
if (!columnNames.has('visitor_id')) {
database.exec("ALTER TABLE visits ADD COLUMN visitor_id TEXT NOT NULL DEFAULT 'legacy'");
columnNames.add('visitor_id');
}
if (!columnNames.has('path')) {
throw new Error('visits table is missing required path column');
}
return {
hasLegacyIpAddress: columnNames.has('ip_address'),
};
}
function getDb() {
if (db) return db;
try {
2026-05-25 09:49:40 -04:00
const dbPath = getDbPath();
db = new Database(dbPath);
2026-05-25 09:49:40 -04:00
db.pragma('busy_timeout = 5000');
db.pragma('journal_mode = WAL');
2026-05-25 09:49:40 -04:00
db.pragma('synchronous = NORMAL');
2026-05-25 09:49:40 -04:00
const schema = ensureSchema(db);
insertStmt = schema.hasLegacyIpAddress
? db.prepare("INSERT INTO visits (ip_address, visitor_id, path) VALUES ('redacted', ?, ?)")
: db.prepare('INSERT INTO visits (visitor_id, path) VALUES (?, ?)');
return db;
} catch (e) {
console.error('Failed to initialize SQLite database:', e);
db = null;
insertStmt = null;
return null;
}
}
2026-05-25 09:49:40 -04:00
export function logVisit(clientAddress: string, userAgent: string, visitPath: string) {
const database = getDb();
2026-05-25 09:49:40 -04:00
if (!database || !insertStmt) return null;
const visitorId = createVisitorId(clientAddress, userAgent);
insertStmt.run(visitorId, visitPath);
return visitorId;
}