incognitolm commited on
Commit
ad573c0
·
1 Parent(s): 670ebb9

Account Handling

Browse files
public/js/modals.js CHANGED
@@ -126,8 +126,8 @@ export function openAuthModal(initialTab = 'signin') {
126
  }
127
  });
128
 
129
- b.querySelector('#github-btn').addEventListener('click', () => loginWithOAuth('github'));
130
- b.querySelector('#google-btn').addEventListener('click', () => loginWithOAuth('google'));
131
  b.querySelector('#forgot-pw').addEventListener('click', () => openForgotPasswordModal());
132
 
133
  // Enter key
 
126
  }
127
  });
128
 
129
+ b.querySelector('#github-btn').addEventListener('click', () => { loginWithOAuth('github'); closeModal(); });
130
+ b.querySelector('#google-btn').addEventListener('click', () => { loginWithOAuth('google'); closeModal(); });
131
  b.querySelector('#forgot-pw').addEventListener('click', () => openForgotPasswordModal());
132
 
133
  // Enter key
public/oauth-callback.html CHANGED
@@ -58,21 +58,9 @@
58
  }
59
 
60
  msgEl.textContent = 'Sign-in complete! Closing…';
61
- // Give localStorage a moment to propagate, then close.
62
  setTimeout(() => {
63
- // If we're in a popup, close it.
64
- if (window.opener !== null || window.history.length <= 1) {
65
- window.close();
66
- }
67
- // If close() didn't work (e.g. tab opened directly), redirect home.
68
- setTimeout(() => {
69
- const p = new URLSearchParams({
70
- oauth: '1',
71
- t: tokens.access_token,
72
- r: tokens.refresh_token || '',
73
- });
74
- location.replace('/?' + p.toString());
75
- }, 800);
76
  }, 300);
77
  } else {
78
  msgEl.textContent = 'No token received. You can close this window.';
 
58
  }
59
 
60
  msgEl.textContent = 'Sign-in complete! Closing…';
61
+ // Give localStorage a moment to propagate, then try to close the popup.
62
  setTimeout(() => {
63
+ try { window.close(); } catch (_) { /* ignore */ }
 
 
 
 
 
 
 
 
 
 
 
 
64
  }, 300);
65
  } else {
66
  msgEl.textContent = 'No token received. You can close this window.';
server/cryptoUtils.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ const ALGORITHM = 'aes-256-gcm';
6
+ const KEY_LENGTH = 32; // 256 bits
7
+ const IV_LENGTH = 16; // 128 bits for GCM
8
+ const AUTH_TAG_LENGTH = 16; // 128 bits
9
+
10
+ // Derive key from environment variable
11
+ function getKey() {
12
+ const keyEnv = process.env.DATA_ENCRYPTION_KEY;
13
+ if (!keyEnv) throw new Error('DATA_ENCRYPTION_KEY environment variable not set');
14
+ // Use SHA-256 to derive a 32-byte key from the env var
15
+ return crypto.createHash('sha256').update(keyEnv).digest();
16
+ }
17
+
18
+ export function encryptJson(data) {
19
+ const key = getKey();
20
+ const iv = crypto.randomBytes(IV_LENGTH);
21
+ const cipher = crypto.createCipher(ALGORITHM, key);
22
+ cipher.setAAD(Buffer.from('')); // Optional AAD
23
+
24
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
25
+ encrypted += cipher.final('hex');
26
+
27
+ const authTag = cipher.getAuthTag();
28
+ return {
29
+ iv: iv.toString('hex'),
30
+ encrypted,
31
+ authTag: authTag.toString('hex'),
32
+ };
33
+ }
34
+
35
+ export function decryptJson(encryptedData) {
36
+ const key = getKey();
37
+ const { iv, encrypted, authTag } = encryptedData;
38
+ const decipher = crypto.createDecipher(ALGORITHM, key);
39
+ decipher.setAuthTag(Buffer.from(authTag, 'hex'));
40
+ decipher.setAAD(Buffer.from('')); // Match AAD
41
+
42
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
43
+ decrypted += decipher.final('utf8');
44
+ return JSON.parse(decrypted);
45
+ }
46
+
47
+ export async function saveEncryptedJson(filePath, data) {
48
+ const encrypted = encryptJson(data);
49
+ await fs.writeFile(filePath, JSON.stringify(encrypted));
50
+ }
51
+
52
+ export async function loadEncryptedJson(filePath) {
53
+ try {
54
+ const content = await fs.readFile(filePath, 'utf8');
55
+ const encrypted = JSON.parse(content);
56
+ return decryptJson(encrypted);
57
+ } catch (err) {
58
+ if (err.code === 'ENOENT') return null; // File not found
59
+ throw err;
60
+ }
61
+ }
server/sessionStore.js CHANGED
@@ -2,6 +2,8 @@
2
  // Device sessions live in memory only (restart clears them).
3
  import { createClient } from '@supabase/supabase-js';
4
  import crypto from 'crypto';
 
 
5
 
6
  let _SUPABASE_URL, _SUPABASE_ANON_KEY;
7
  export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; }
@@ -14,11 +16,45 @@ const userCache = new Map(); // userId -> { sessions: Map, online: Set }
14
  const tempStore = new Map(); // tempId -> TempData
15
  const devSessions = new Map(); // token -> DeviceSession
16
 
17
- setInterval(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const now = Date.now();
19
  for (const [id, d] of tempStore)
20
  if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY)
21
  tempStore.delete(id);
 
 
22
  }, 30 * 60 * 1000);
23
 
24
  function userClient(accessToken) {
@@ -42,15 +78,19 @@ export const sessionStore = {
42
  createTempSession(t) {
43
  const d = this.initTemp(t);
44
  const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
45
- d.sessions.set(s.id, s); d.lastActive = Date.now(); return s;
 
 
46
  },
47
  updateTempSession(t, id, patch) {
48
  const d = tempStore.get(t); if (!d) return null;
49
  const s = d.sessions.get(id); if (!s) return null;
50
- Object.assign(s, patch); d.lastActive = Date.now(); return s;
 
 
51
  },
52
- deleteTempSession(t, id) { tempStore.get(t)?.sessions.delete(id); },
53
- deleteTempAll(t) { tempStore.get(t)?.sessions.clear(); },
54
 
55
  /**
56
  * Copy temp sessions into the user's account on login.
 
2
  // Device sessions live in memory only (restart clears them).
3
  import { createClient } from '@supabase/supabase-js';
4
  import crypto from 'crypto';
5
+ import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js';
6
+ import path from 'path';
7
 
8
  let _SUPABASE_URL, _SUPABASE_ANON_KEY;
9
  export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; }
 
16
  const tempStore = new Map(); // tempId -> TempData
17
  const devSessions = new Map(); // token -> DeviceSession
18
 
19
+ const TEMP_STORE_FILE = '/data/temp_sessions.json';
20
+
21
+ async function loadTempStore() {
22
+ const data = await loadEncryptedJson(TEMP_STORE_FILE);
23
+ if (data) {
24
+ for (const [id, d] of Object.entries(data)) {
25
+ tempStore.set(id, {
26
+ sessions: new Map(Object.entries(d.sessions || {})),
27
+ msgCount: d.msgCount || 0,
28
+ created: d.created || Date.now(),
29
+ lastActive: d.lastActive || Date.now(),
30
+ });
31
+ }
32
+ }
33
+ }
34
+
35
+ async function saveTempStore() {
36
+ const data = {};
37
+ for (const [id, d] of tempStore) {
38
+ data[id] = {
39
+ sessions: Object.fromEntries(d.sessions),
40
+ msgCount: d.msgCount,
41
+ created: d.created,
42
+ lastActive: d.lastActive,
43
+ };
44
+ }
45
+ await saveEncryptedJson(TEMP_STORE_FILE, data);
46
+ }
47
+
48
+ // Load temp store on init
49
+ loadTempStore().catch(err => console.error('Failed to load temp store:', err));
50
+
51
+ setInterval(async () => {
52
  const now = Date.now();
53
  for (const [id, d] of tempStore)
54
  if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY)
55
  tempStore.delete(id);
56
+ // Save after cleanup
57
+ await saveTempStore().catch(err => console.error('Failed to save temp store:', err));
58
  }, 30 * 60 * 1000);
59
 
60
  function userClient(accessToken) {
 
78
  createTempSession(t) {
79
  const d = this.initTemp(t);
80
  const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
81
+ d.sessions.set(s.id, s); d.lastActive = Date.now();
82
+ saveTempStore().catch(err => console.error('Failed to save temp store:', err));
83
+ return s;
84
  },
85
  updateTempSession(t, id, patch) {
86
  const d = tempStore.get(t); if (!d) return null;
87
  const s = d.sessions.get(id); if (!s) return null;
88
+ Object.assign(s, patch); d.lastActive = Date.now();
89
+ saveTempStore().catch(err => console.error('Failed to save temp store:', err));
90
+ return s;
91
  },
92
+ deleteTempSession(t, id) { tempStore.get(t)?.sessions.delete(id); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); },
93
+ deleteTempAll(t) { tempStore.get(t)?.sessions.clear(); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); },
94
 
95
  /**
96
  * Copy temp sessions into the user's account on login.
server/wsHandler.js CHANGED
@@ -56,7 +56,9 @@ const handlers = {
56
  client.deviceToken = deviceSessionStore.create(user.id, client.ip, client.userAgent);
57
  sessionStore.markOnline(user.id, ws);
58
 
59
- const tId = clientTempId || client.tempId;
 
 
60
  await sessionStore.transferTempToUser(tId, user.id, accessToken);
61
 
62
  const [sessions, settings, profile] = await Promise.all([
@@ -79,7 +81,9 @@ const handlers = {
79
  },
80
 
81
  'auth:guest': (ws, msg, client) => {
 
82
  const t = msg.tempId || client.tempId;
 
83
  sessionStore.initTemp(t);
84
  safeSend(ws, { type: 'auth:guestOk', tempId: t, sessions: sessionStore.getTempSessions(t).map(ser) });
85
  },
 
56
  client.deviceToken = deviceSessionStore.create(user.id, client.ip, client.userAgent);
57
  sessionStore.markOnline(user.id, ws);
58
 
59
+ // If the client provided a persistent tempId, use and save it on the ws client
60
+ if (clientTempId) client.tempId = clientTempId;
61
+ const tId = client.tempId;
62
  await sessionStore.transferTempToUser(tId, user.id, accessToken);
63
 
64
  const [sessions, settings, profile] = await Promise.all([
 
81
  },
82
 
83
  'auth:guest': (ws, msg, client) => {
84
+ // Honor a client-supplied persistent tempId and persist it on the ws client
85
  const t = msg.tempId || client.tempId;
86
+ client.tempId = t;
87
  sessionStore.initTemp(t);
88
  safeSend(ws, { type: 'auth:guestOk', tempId: t, sessions: sessionStore.getTempSessions(t).map(ser) });
89
  },