const Database = require('better-sqlite3') const path = require('path') const fs = require('fs') const crypto = require('crypto') const dataDir = path.join(process.cwd(), 'data') if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) const dbPath = path.join(dataDir, 'rational_lock.db') const db = new Database(dbPath) db.exec(` CREATE TABLE IF NOT EXISTS rl_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS rl_sessions ( token TEXT PRIMARY KEY, username TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS rl_user_data ( username TEXT PRIMARY KEY, items_json TEXT NOT NULL DEFAULT '[]', applications_json TEXT NOT NULL DEFAULT '[]', draft_json TEXT, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_rl_sessions_user ON rl_sessions(username); CREATE INDEX IF NOT EXISTS idx_rl_sessions_exp ON rl_sessions(expires_at); `) const now = () => Date.now() const SESSION_MAX_AGE = 1 * 24 * 3600 * 1000 const COOKIE_NAME = 'rational_lock_sid' const DEFAULT_USER = { username: 'admin_rl', password: process.env.LOCK_DEFAULT_PASSWORD || '' } const parseCookie = cookieStr => { const out = {} String(cookieStr || '').split(/;\s*/).forEach(p => { const idx = p.indexOf('=') if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1)) }) return out } const ensureDefaultUser = () => { const row = db.prepare('SELECT username FROM rl_users WHERE username = ?').get(DEFAULT_USER.username) if (!row) { db.prepare('INSERT INTO rl_users (username, password, created_at) VALUES (?, ?, ?)').run(DEFAULT_USER.username, DEFAULT_USER.password, now()) } } const cleanupSessions = () => { db.prepare('DELETE FROM rl_sessions WHERE expires_at <= ?').run(now()) } const readDataOf = username => { const row = db.prepare('SELECT items_json, applications_json, draft_json FROM rl_user_data WHERE username = ?').get(username) if (!row) return { db: { items: [], applications: [] }, draft: null } let items = [] let applications = [] let draft = null try { items = JSON.parse(row.items_json || '[]') } catch {} try { applications = JSON.parse(row.applications_json || '[]') } catch {} try { draft = row.draft_json ? JSON.parse(row.draft_json) : null } catch {} return { db: { items: Array.isArray(items) ? items : [], applications: Array.isArray(applications) ? applications : [] }, draft: draft && typeof draft === 'object' ? draft : null } } const writeDataOf = (username, data) => { const dbData = data && data.db && typeof data.db === 'object' ? data.db : {} const items = Array.isArray(dbData.items) ? dbData.items : [] const applications = Array.isArray(dbData.applications) ? dbData.applications : [] const draft = data && data.draft && typeof data.draft === 'object' ? data.draft : null db.prepare(` INSERT INTO rl_user_data (username, items_json, applications_json, draft_json, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(username) DO UPDATE SET items_json = excluded.items_json, applications_json = excluded.applications_json, draft_json = excluded.draft_json, updated_at = excluded.updated_at `).run( username, JSON.stringify(items), JSON.stringify(applications), draft ? JSON.stringify(draft) : null, now() ) } const getSessionUser = req => { cleanupSessions() const cookies = parseCookie(req.headers.cookie || '') const token = String(cookies[COOKIE_NAME] || '') if (!token) return '' const row = db.prepare('SELECT username, expires_at FROM rl_sessions WHERE token = ?').get(token) if (!row) return '' if (Number(row.expires_at) <= now()) { db.prepare('DELETE FROM rl_sessions WHERE token = ?').run(token) return '' } return String(row.username || '') } const requireAuth = (req, res, next) => { const username = getSessionUser(req) if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' }) req.rlUser = username next() } const bindRoutes = app => { ensureDefaultUser() app.get('/api/rational_lock/health', (req, res) => { res.json({ ok: true }) }) app.get('/api/rational_lock/auth/local_creds', (req, res) => { try { const os = require('os') const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir() const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')] const usersRoot = path.join(path.parse(userProfile).root, 'Users') try { if (fs.existsSync(usersRoot)) { fs.readdirSync(usersRoot).forEach(name => { const dir = path.join(usersRoot, name) if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local')) }) } } catch {} let content = null for (const p of candidates) { if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break } } if (!content) return res.json({ ok: true, creds: null }) const creds = {} content.split(/\r?\n/).forEach(line => { const t = line.trim() if (!t || t.startsWith('#')) return const idx = t.indexOf('=') if (idx > 0) creds[t.slice(0, idx).trim()] = t.slice(idx + 1).trim() }) const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '') if (serverToken) { const clientToken = String(req.query.token || '') if (clientToken !== serverToken) return res.json({ ok: true, creds: null }) } const u = creds.RL_USERNAME || '' const p = creds.RL_PASSWORD || '' if (!u && !p) return res.json({ ok: true, creds: null }) res.json({ ok: true, creds: { username: u, password: p } }) } catch { res.json({ ok: true, creds: null }) } }) app.post('/api/rational_lock/auth/login', (req, res) => { const username = String((req.body && req.body.username) || '').trim() const password = String((req.body && req.body.password) || '') if (!username || !password) return res.status(400).json({ ok: false, error: 'invalid_credentials' }) const row = db.prepare('SELECT username, password FROM rl_users WHERE username = ?').get(username) if (!row || String(row.password || '') !== password) return res.status(401).json({ ok: false, error: 'invalid_credentials' }) const token = crypto.randomBytes(24).toString('hex') const createdAt = now() const expiresAt = createdAt + SESSION_MAX_AGE db.prepare('INSERT INTO rl_sessions (token, username, created_at, expires_at) VALUES (?, ?, ?, ?)').run(token, username, createdAt, expiresAt) res.cookie(COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE }) res.json({ ok: true, username }) }) app.post('/api/rational_lock/auth/logout', (req, res) => { const cookies = parseCookie(req.headers.cookie || '') const token = String(cookies[COOKIE_NAME] || '') if (token) db.prepare('DELETE FROM rl_sessions WHERE token = ?').run(token) res.cookie(COOKIE_NAME, '', { httpOnly: true, sameSite: 'lax', maxAge: 0 }) res.json({ ok: true }) }) app.get('/api/rational_lock/auth/me', (req, res) => { const username = getSessionUser(req) if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' }) res.json({ ok: true, username }) }) app.get('/api/rational_lock/data', requireAuth, (req, res) => { const payload = readDataOf(req.rlUser) res.json({ ok: true, ...payload }) }) app.put('/api/rational_lock/data', requireAuth, (req, res) => { writeDataOf(req.rlUser, req.body || {}) res.json({ ok: true }) }) } module.exports = { bindRoutes }