This commit is contained in:
98
lib/db.ts
98
lib/db.ts
@@ -2,39 +2,86 @@ import Database from 'better-sqlite3';
|
||||
import type BetterSqlite3 from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
let db: BetterSqlite3.Database | null = null;
|
||||
let insertStmt: BetterSqlite3.Statement | null = null;
|
||||
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 {
|
||||
const SERVER_STORAGE = '/server_storage';
|
||||
let dbDir: string;
|
||||
try {
|
||||
fs.accessSync(SERVER_STORAGE, fs.constants.W_OK);
|
||||
dbDir = SERVER_STORAGE;
|
||||
} catch {
|
||||
dbDir = process.cwd();
|
||||
}
|
||||
|
||||
const dbPath = path.join(dbDir, 'visitors.db');
|
||||
const dbPath = getDbPath();
|
||||
db = new Database(dbPath);
|
||||
db.pragma('busy_timeout = 5000');
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS visits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
visited_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
const schema = ensureSchema(db);
|
||||
|
||||
insertStmt = db.prepare(
|
||||
'INSERT INTO visits (ip_address, path) VALUES (?, ?)'
|
||||
);
|
||||
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) {
|
||||
@@ -45,8 +92,11 @@ function getDb() {
|
||||
}
|
||||
}
|
||||
|
||||
export function logVisit(ip: string, visitPath: string) {
|
||||
export function logVisit(clientAddress: string, userAgent: string, visitPath: string) {
|
||||
const database = getDb();
|
||||
if (!database || !insertStmt) return;
|
||||
insertStmt.run(ip, visitPath);
|
||||
if (!database || !insertStmt) return null;
|
||||
|
||||
const visitorId = createVisitorId(clientAddress, userAgent);
|
||||
insertStmt.run(visitorId, visitPath);
|
||||
return visitorId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user