mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
activity_log: stop the bleeding - writer-leak fix on 3 sites (activityLogger middleware, alert service, login route) + one-time backfill of 548 NULL-workspace rows via device.workspace_id or workspace_members lookup; activity.js route migration deferred to its own slice tomorrow.
KNOWN REGRESSION (Phase 3 fix): platform_admin / superadmin no longer has cross-workspace 'see everything' view. Every route migrated tonight (2.2a-2.2m) deliberately removed the role-based bypass per design doc - cross-workspace visibility will come via dedicated admin endpoints in Phase 3, not magic role bypasses. Until Phase 3 ships, platform admins must switch-workspace to see other workspaces' data.
This commit is contained in:
parent
f88805f36d
commit
88d91b10af
|
|
@ -370,6 +370,51 @@ function migrateFolderWorkspaceIds() {
|
||||||
|
|
||||||
migrateFolderWorkspaceIds();
|
migrateFolderWorkspaceIds();
|
||||||
|
|
||||||
|
const PHASE_2_2_ACTIVITY_STOP_ID = 'phase_2_2_activity_log_stop_bleeding';
|
||||||
|
|
||||||
|
// One-time backfill of activity_log rows that were written between the
|
||||||
|
// Phase 1 schema migration and the writer-leak fix in this commit. Strategy:
|
||||||
|
// * Rows with device_id: derive workspace_id from devices.workspace_id
|
||||||
|
// (the activity is about a specific device, so this is unambiguous).
|
||||||
|
// * Rows with no device_id but a user_id: derive from the user's oldest
|
||||||
|
// workspace_members row (pre-flight confirmed 0 affected users have
|
||||||
|
// more than one workspace, so the choice is unambiguous).
|
||||||
|
// Rows with user_id IS NULL (auth:login_failed and similar pre-tenancy
|
||||||
|
// system events) are left alone - they have no tenant context.
|
||||||
|
function backfillActivityLogWorkspace() {
|
||||||
|
const already = db.prepare('SELECT 1 FROM schema_migrations WHERE id = ?').get(PHASE_2_2_ACTIVITY_STOP_ID);
|
||||||
|
if (already) return;
|
||||||
|
|
||||||
|
const viaDevice = db.prepare(`
|
||||||
|
UPDATE activity_log SET workspace_id = (
|
||||||
|
SELECT workspace_id FROM devices WHERE devices.id = activity_log.device_id
|
||||||
|
)
|
||||||
|
WHERE workspace_id IS NULL AND device_id IS NOT NULL
|
||||||
|
AND EXISTS (SELECT 1 FROM devices WHERE devices.id = activity_log.device_id AND devices.workspace_id IS NOT NULL)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const viaMembers = db.prepare(`
|
||||||
|
UPDATE activity_log SET workspace_id = (
|
||||||
|
SELECT wm.workspace_id FROM workspace_members wm
|
||||||
|
WHERE wm.user_id = activity_log.user_id
|
||||||
|
ORDER BY wm.joined_at ASC LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE workspace_id IS NULL AND user_id IS NOT NULL AND device_id IS NULL
|
||||||
|
AND EXISTS (SELECT 1 FROM workspace_members wm WHERE wm.user_id = activity_log.user_id)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
const d = viaDevice.run().changes;
|
||||||
|
const m = viaMembers.run().changes;
|
||||||
|
db.prepare('INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)').run(PHASE_2_2_ACTIVITY_STOP_ID);
|
||||||
|
return { d, m };
|
||||||
|
});
|
||||||
|
const { d, m } = tx();
|
||||||
|
if (d + m > 0) console.log(`activity_log backfill: ${d} via device.workspace_id, ${m} via workspace_members lookup`);
|
||||||
|
}
|
||||||
|
|
||||||
|
backfillActivityLogWorkspace();
|
||||||
|
|
||||||
// Prune old telemetry (keep last 24h worth at 15s intervals = ~5760, cap at 6000)
|
// Prune old telemetry (keep last 24h worth at 15s intervals = ~5760, cap at 6000)
|
||||||
function pruneTelemetry(deviceId) {
|
function pruneTelemetry(deviceId) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,15 @@ function logFailedLogin(email, ip, reason) {
|
||||||
|
|
||||||
function logSuccessfulLogin(userId, email, ip) {
|
function logSuccessfulLogin(userId, email, ip) {
|
||||||
try {
|
try {
|
||||||
db.prepare('INSERT INTO activity_log (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)')
|
// Phase 2.2 writer-leak fix: stamp the user's oldest workspace so this
|
||||||
.run(userId, 'auth:login_success', email, ip);
|
// login event is queryable in tenant-scoped activity views. Multi-workspace
|
||||||
|
// users still land on one row; the activity dashboard already shows
|
||||||
|
// per-user context separately from per-workspace context.
|
||||||
|
const ws = db.prepare(
|
||||||
|
'SELECT workspace_id FROM workspace_members WHERE user_id = ? ORDER BY joined_at ASC LIMIT 1'
|
||||||
|
).get(userId);
|
||||||
|
db.prepare('INSERT INTO activity_log (user_id, action, details, ip_address, workspace_id) VALUES (?, ?, ?, ?, ?)')
|
||||||
|
.run(userId, 'auth:login_success', email, ip, ws?.workspace_id || null);
|
||||||
db.prepare("UPDATE users SET last_login = strftime('%s','now') WHERE id = ?").run(userId);
|
db.prepare("UPDATE users SET last_login = strftime('%s','now') WHERE id = ?").run(userId);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,21 @@ function getClientIp(req) {
|
||||||
return req.ip || null;
|
return req.ip || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function logActivity(userId, action, details = null, deviceId = null, ipAddress = null) {
|
// Phase 2.2 writer-leak fix: activity_log rows now stamp workspace_id so
|
||||||
|
// tenant-scoped queries don't miss new events. Callers pass the workspace
|
||||||
|
// when known; the middleware below sources it from resolveTenancy. When
|
||||||
|
// workspaceId is null but a device_id is provided, fall back to the device's
|
||||||
|
// workspace - matches the backfill rule for consistency.
|
||||||
|
function logActivity(userId, action, details = null, deviceId = null, ipAddress = null, workspaceId = null) {
|
||||||
try {
|
try {
|
||||||
|
let ws = workspaceId || null;
|
||||||
|
if (!ws && deviceId) {
|
||||||
|
const d = db.prepare('SELECT workspace_id FROM devices WHERE id = ?').get(deviceId);
|
||||||
|
ws = d?.workspace_id || null;
|
||||||
|
}
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO activity_log (user_id, device_id, action, details, ip_address) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO activity_log (user_id, device_id, action, details, ip_address, workspace_id) VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
).run(userId || null, deviceId || null, action, details || null, ipAddress || null);
|
).run(userId || null, deviceId || null, action, details || null, ipAddress || null, ws);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Activity log error:', e.message);
|
console.error('Activity log error:', e.message);
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +77,7 @@ function activityLogger(req, res, next) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
const deviceId = req.params?.id || req.params?.deviceId || req.body?.device_id;
|
const deviceId = req.params?.id || req.params?.deviceId || req.body?.device_id;
|
||||||
const details = summarizeAction(req);
|
const details = summarizeAction(req);
|
||||||
logActivity(userId, action, details, deviceId, getClientIp(req));
|
logActivity(userId, action, details, deviceId, getClientIp(req), req.workspaceId || null);
|
||||||
}
|
}
|
||||||
return originalJson(data);
|
return originalJson(data);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ function checkOfflineDevices(io) {
|
||||||
const threshold = 300; // 5 minutes offline
|
const threshold = 300; // 5 minutes offline
|
||||||
|
|
||||||
const offlineDevices = db.prepare(`
|
const offlineDevices = db.prepare(`
|
||||||
SELECT d.id, d.name, d.user_id, d.last_heartbeat, d.status,
|
SELECT d.id, d.name, d.user_id, d.workspace_id, d.last_heartbeat, d.status,
|
||||||
u.email as owner_email, u.name as owner_name, u.email_alerts
|
u.email as owner_email, u.name as owner_name, u.email_alerts
|
||||||
FROM devices d
|
FROM devices d
|
||||||
LEFT JOIN users u ON d.user_id = u.id
|
LEFT JOIN users u ON d.user_id = u.id
|
||||||
|
|
@ -42,11 +42,12 @@ function checkOfflineDevices(io) {
|
||||||
});
|
});
|
||||||
offlineNotified.set(device.id, now);
|
offlineNotified.set(device.id, now);
|
||||||
|
|
||||||
// Log activity
|
// Log activity. Phase 2.2 writer-leak fix: stamp workspace_id from the
|
||||||
|
// device so the row is tenant-queryable.
|
||||||
try {
|
try {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO activity_log (user_id, device_id, action, details) VALUES (?, ?, ?, ?)'
|
'INSERT INTO activity_log (user_id, device_id, action, details, workspace_id) VALUES (?, ?, ?, ?, ?)'
|
||||||
).run(device.user_id, device.id, 'alert:device_offline', `${device.name} offline for ${offlineMinutes}m`);
|
).run(device.user_id, device.id, 'alert:device_offline', `${device.name} offline for ${offlineMinutes}m`, device.workspace_id || null);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue