Add snapshot management to API and enhance user-agent validation for v2 routes
This commit is contained in:
130
db.js
130
db.js
@@ -35,6 +35,21 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at);
|
||||
`);
|
||||
|
||||
// snapshots table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS snapshots (
|
||||
token TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
accessed_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshots_accessed_at ON snapshots(accessed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshots_session_id ON snapshots(session_id);
|
||||
`);
|
||||
|
||||
// migration: add verified column if it doesn't exist
|
||||
try {
|
||||
db.exec(`ALTER TABLE sessions ADD COLUMN verified INTEGER DEFAULT 0`);
|
||||
@@ -51,6 +66,14 @@ function nowEpoch() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function generateSnapshotToken() {
|
||||
const buffer = crypto.randomBytes(16);
|
||||
return buffer.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '~');
|
||||
}
|
||||
|
||||
// convert db row to api resposne format
|
||||
function rowToSession(row) {
|
||||
if (!row) return null;
|
||||
@@ -130,15 +153,15 @@ function updateSession(id, data) {
|
||||
|
||||
if (data.displayName !== undefined) {
|
||||
updates.push('display_name = ?');
|
||||
values.push(data.displayName);
|
||||
values.push(data.displayName || 'Anonymous');
|
||||
}
|
||||
if (data.username !== undefined) {
|
||||
updates.push('username = ?');
|
||||
values.push(data.username);
|
||||
values.push(data.username || '@anonymous');
|
||||
}
|
||||
if (data.text !== undefined) {
|
||||
updates.push('text = ?');
|
||||
values.push(data.text);
|
||||
values.push(data.text || 'No text provided');
|
||||
}
|
||||
if (data.avatarUrl !== undefined) {
|
||||
updates.push('avatar_url = ?');
|
||||
@@ -203,6 +226,98 @@ function deleteSession(id) {
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
function createSnapshot(sessionId, config, maxRetries = 3) {
|
||||
const configJson = JSON.stringify(config);
|
||||
const now = nowEpoch();
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const token = generateSnapshotToken();
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO snapshots (token, session_id, config_json, created_at, accessed_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(token, sessionId, configJson, now, now);
|
||||
|
||||
return {
|
||||
token: token,
|
||||
sessionId: sessionId,
|
||||
configJson: configJson,
|
||||
createdAt: now,
|
||||
accessedAt: now
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code === 'SQLITE_CONSTRAINT' && attempt < maxRetries - 1) {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to generate unique snapshot token after retries');
|
||||
}
|
||||
|
||||
function getSnapshot(token) {
|
||||
const stmt = db.prepare('SELECT * FROM snapshots WHERE token = ?');
|
||||
const row = stmt.get(token);
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
token: row.token,
|
||||
sessionId: row.session_id,
|
||||
configJson: row.config_json,
|
||||
createdAt: row.created_at,
|
||||
accessedAt: row.accessed_at
|
||||
};
|
||||
}
|
||||
|
||||
function touchSnapshot(token) {
|
||||
const now = nowEpoch();
|
||||
const stmt = db.prepare('UPDATE snapshots SET accessed_at = ? WHERE token = ?');
|
||||
const result = stmt.run(now, token);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
function deleteSnapshot(token) {
|
||||
const stmt = db.prepare('DELETE FROM snapshots WHERE token = ?');
|
||||
const result = stmt.run(token);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
function getSnapshotsForSession(sessionId) {
|
||||
const stmt = db.prepare('SELECT * FROM snapshots WHERE session_id = ? ORDER BY created_at DESC');
|
||||
const rows = stmt.all(sessionId);
|
||||
|
||||
return rows.map(row => ({
|
||||
token: row.token,
|
||||
sessionId: row.session_id,
|
||||
configJson: row.config_json,
|
||||
createdAt: row.created_at,
|
||||
accessedAt: row.accessed_at
|
||||
}));
|
||||
}
|
||||
|
||||
function cleanupExpiredSnapshots() {
|
||||
const FORTY_EIGHT_HOURS = 48 * 60 * 60;
|
||||
const cutoff = nowEpoch() - FORTY_EIGHT_HOURS;
|
||||
|
||||
const stmt = db.prepare('DELETE FROM snapshots WHERE accessed_at < ?');
|
||||
const result = stmt.run(cutoff);
|
||||
|
||||
if (result.changes > 0) {
|
||||
console.log(`Cleaned up ${result.changes} expired snapshot(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
function countSnapshotsForSession(sessionId) {
|
||||
const stmt = db.prepare('SELECT COUNT(*) as count FROM snapshots WHERE session_id = ?');
|
||||
const row = stmt.get(sessionId);
|
||||
return row.count;
|
||||
}
|
||||
|
||||
|
||||
function cleanupExpiredSessions() {
|
||||
const ONE_DAY = 24 * 60 * 60;
|
||||
@@ -221,5 +336,12 @@ module.exports = {
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
cleanupExpiredSessions
|
||||
cleanupExpiredSessions,
|
||||
createSnapshot,
|
||||
getSnapshot,
|
||||
touchSnapshot,
|
||||
deleteSnapshot,
|
||||
getSnapshotsForSession,
|
||||
cleanupExpiredSnapshots,
|
||||
countSnapshotsForSession
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user