Files
comaps/webapp/src/database.js
2025-10-25 15:38:41 -07:00

241 lines
6.1 KiB
JavaScript

const Database = require('better-sqlite3');
const path = require('path');
class LocationDatabase {
constructor(dbPath) {
this.db = new Database(dbPath || path.join(__dirname, '..', 'location_sharing.db'));
this.initSchema();
this.setupCleanup();
}
initSchema() {
// Create sessions table
this.db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
last_update INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
is_active INTEGER DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_sessions_active
ON sessions(is_active, expires_at);
`);
// Create location_updates table
this.db.exec(`
CREATE TABLE IF NOT EXISTS location_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
encrypted_payload TEXT NOT NULL,
timestamp INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_location_session
ON location_updates(session_id, timestamp DESC);
`);
}
/**
* Create a new session
*/
createSession(sessionId, expiryHours = 24) {
const now = Date.now();
const expiresAt = now + (expiryHours * 60 * 60 * 1000);
const stmt = this.db.prepare(`
INSERT INTO sessions (session_id, created_at, last_update, expires_at, is_active)
VALUES (?, ?, ?, ?, 1)
`);
try {
stmt.run(sessionId, now, now, expiresAt);
return { success: true, sessionId };
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
// Session already exists, update it
return this.reactivateSession(sessionId, expiryHours);
}
throw err;
}
}
/**
* Reactivate an existing session
*/
reactivateSession(sessionId, expiryHours = 24) {
const now = Date.now();
const expiresAt = now + (expiryHours * 60 * 60 * 1000);
const stmt = this.db.prepare(`
UPDATE sessions
SET is_active = 1, last_update = ?, expires_at = ?
WHERE session_id = ?
`);
const result = stmt.run(now, expiresAt, sessionId);
return {
success: result.changes > 0,
sessionId,
reactivated: true
};
}
/**
* Store a location update
*/
storeLocationUpdate(sessionId, encryptedPayload) {
const now = Date.now();
// First, check if session exists and is active
const session = this.getSession(sessionId);
if (!session || !session.is_active || session.expires_at < now) {
return { success: false, error: 'Session not found or expired' };
}
// Update session last_update timestamp
const updateSession = this.db.prepare(`
UPDATE sessions SET last_update = ? WHERE session_id = ?
`);
updateSession.run(now, sessionId);
// Insert location update
const stmt = this.db.prepare(`
INSERT INTO location_updates (session_id, encrypted_payload, timestamp)
VALUES (?, ?, ?)
`);
try {
const result = stmt.run(sessionId, encryptedPayload, now);
return {
success: true,
updateId: result.lastInsertRowid
};
} catch (err) {
return { success: false, error: err.message };
}
}
/**
* Get session info
*/
getSession(sessionId) {
const stmt = this.db.prepare(`
SELECT * FROM sessions WHERE session_id = ?
`);
return stmt.get(sessionId);
}
/**
* Get latest location update for a session
*/
getLatestLocation(sessionId) {
const stmt = this.db.prepare(`
SELECT encrypted_payload, timestamp
FROM location_updates
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT 1
`);
return stmt.get(sessionId);
}
/**
* Get location history for a session
*/
getLocationHistory(sessionId, limit = 100) {
const stmt = this.db.prepare(`
SELECT encrypted_payload, timestamp
FROM location_updates
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT ?
`);
return stmt.all(sessionId, limit);
}
/**
* Stop a session
*/
stopSession(sessionId) {
const stmt = this.db.prepare(`
UPDATE sessions SET is_active = 0 WHERE session_id = ?
`);
const result = stmt.run(sessionId);
return { success: result.changes > 0 };
}
/**
* Clean up expired sessions and old location updates
*/
cleanup() {
const now = Date.now();
const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000);
// Mark expired sessions as inactive
const expireStmt = this.db.prepare(`
UPDATE sessions
SET is_active = 0
WHERE expires_at < ? AND is_active = 1
`);
const expired = expireStmt.run(now);
// Delete old inactive sessions (older than 1 week)
const deleteSessionsStmt = this.db.prepare(`
DELETE FROM sessions
WHERE is_active = 0 AND expires_at < ?
`);
const deletedSessions = deleteSessionsStmt.run(oneWeekAgo);
// Delete orphaned location updates (sessions deleted by CASCADE)
// This is automatic due to CASCADE, but we can also delete old updates
const deleteUpdatesStmt = this.db.prepare(`
DELETE FROM location_updates
WHERE timestamp < ?
`);
const deletedUpdates = deleteUpdatesStmt.run(oneWeekAgo);
return {
expiredSessions: expired.changes,
deletedSessions: deletedSessions.changes,
deletedUpdates: deletedUpdates.changes
};
}
/**
* Setup automatic cleanup
*/
setupCleanup(intervalMinutes = 60) {
setInterval(() => {
const result = this.cleanup();
console.log('[DB Cleanup]', new Date().toISOString(), result);
}, intervalMinutes * 60 * 1000);
}
/**
* Get database statistics
*/
getStats() {
const sessionCount = this.db.prepare(`
SELECT COUNT(*) as count FROM sessions WHERE is_active = 1
`).get();
const totalUpdates = this.db.prepare(`
SELECT COUNT(*) as count FROM location_updates
`).get();
return {
activeSessions: sessionCount.count,
totalLocationUpdates: totalUpdates.count
};
}
close() {
this.db.close();
}
}
module.exports = LocationDatabase;