Luca448 commited on
Commit
dfd686a
·
1 Parent(s): 3ed9766

Savestate: Fix sync settings overwrite bug and persist crypto key in sessionStorage

Browse files
dev-dist/sw.js CHANGED
@@ -81,7 +81,7 @@ define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
81
  "revision": "3ca0b8505b4bec776b69afdba2768812"
82
  }, {
83
  "url": "index.html",
84
- "revision": "0.3e92u8tignc"
85
  }], {});
86
  workbox.cleanupOutdatedCaches();
87
  workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
 
81
  "revision": "3ca0b8505b4bec776b69afdba2768812"
82
  }, {
83
  "url": "index.html",
84
+ "revision": "0.ch9b1jd5754"
85
  }], {});
86
  workbox.cleanupOutdatedCaches();
87
  workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
dynamischer Name.txt CHANGED
@@ -2,9 +2,17 @@ farbverlauf
2
 
3
  nur bilder absenden können
4
 
 
 
5
  Enter
6
 
7
- wissensammeln --> persönliche news
 
 
 
 
 
 
8
 
9
  groq:
10
 
 
2
 
3
  nur bilder absenden können
4
 
5
+ Database Password: #8BA2V*d4xtPzzv
6
+
7
  Enter
8
 
9
+ meine Themen (Entdecken Sektion)
10
+
11
+ Account system
12
+
13
+ wissensammeln --> persönliche News
14
+
15
+ open router: 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
16
 
17
  groq:
18
 
src/app.js CHANGED
@@ -189,6 +189,7 @@ window.copyToClipboard = async (text, btnElement) => {
189
  const initApp = async () => {
190
  // Wait for auth to initialize
191
  await authService.initialize();
 
192
 
193
  // Scale up UI on web (HuggingFace) to match Capacitor native scale
194
  const isNative = window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform();
 
189
  const initApp = async () => {
190
  // Wait for auth to initialize
191
  await authService.initialize();
192
+ await syncManager.initialize();
193
 
194
  // Scale up UI on web (HuggingFace) to match Capacitor native scale
195
  const isNative = window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform();
src/services/auth-service.js CHANGED
@@ -32,8 +32,8 @@ class AuthService {
32
  // If the crypto key is null, the UI should prompt the user to "Unlock" their data
33
  // by entering their password again, OR we can try to store the derived key in sessionStorage
34
  // (not localStorage!) so it survives page reloads but dies when the tab closes.
35
- const storedKey = sessionStorage.getItem('schoolmind_temp_key_marker');
36
- if (!storedKey) {
37
  // App will need to ask for password to unlock
38
  console.warn('Session active, but crypto key missing. Needs unlock.');
39
  }
 
32
  // If the crypto key is null, the UI should prompt the user to "Unlock" their data
33
  // by entering their password again, OR we can try to store the derived key in sessionStorage
34
  // (not localStorage!) so it survives page reloads but dies when the tab closes.
35
+ const restored = await cryptoService.restoreKey();
36
+ if (!restored) {
37
  // App will need to ask for password to unlock
38
  console.warn('Session active, but crypto key missing. Needs unlock.');
39
  }
src/services/crypto-service.js CHANGED
@@ -29,10 +29,15 @@ class CryptoService {
29
  },
30
  keyMaterial,
31
  { name: 'AES-GCM', length: 256 },
32
- false,
33
  ['encrypt', 'decrypt']
34
  );
35
 
 
 
 
 
 
36
  return true;
37
  }
38
 
@@ -93,8 +98,32 @@ class CryptoService {
93
  }
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  clearKey() {
97
  this.key = null;
 
98
  }
99
 
100
  // --- Utility functions ---
 
29
  },
30
  keyMaterial,
31
  { name: 'AES-GCM', length: 256 },
32
+ true, // make extractable to store in sessionStorage
33
  ['encrypt', 'decrypt']
34
  );
35
 
36
+ // Store in sessionStorage (survives reload, dies on tab close)
37
+ const rawKey = await crypto.subtle.exportKey('raw', this.key);
38
+ const base64Key = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
39
+ sessionStorage.setItem('schoolmind_session_key', base64Key);
40
+
41
  return true;
42
  }
43
 
 
98
  }
99
  }
100
 
101
+ async restoreKey() {
102
+ const base64Key = sessionStorage.getItem('schoolmind_session_key');
103
+ if (!base64Key) return false;
104
+ try {
105
+ const binaryString = atob(base64Key);
106
+ const bytes = new Uint8Array(binaryString.length);
107
+ for (let i = 0; i < binaryString.length; i++) {
108
+ bytes[i] = binaryString.charCodeAt(i);
109
+ }
110
+ this.key = await crypto.subtle.importKey(
111
+ 'raw',
112
+ bytes,
113
+ { name: 'AES-GCM', length: 256 },
114
+ true,
115
+ ['encrypt', 'decrypt']
116
+ );
117
+ return true;
118
+ } catch (e) {
119
+ console.error('Failed to restore crypto key:', e);
120
+ return false;
121
+ }
122
+ }
123
+
124
  clearKey() {
125
  this.key = null;
126
+ sessionStorage.removeItem('schoolmind_session_key');
127
  }
128
 
129
  // --- Utility functions ---
src/services/sync-service.js CHANGED
@@ -19,6 +19,13 @@ class SyncManager {
19
  });
20
  }
21
 
 
 
 
 
 
 
 
22
  async sync() {
23
  if (!authService.user || !authService.isCryptoUnlocked() || this.isSyncing) return;
24
  this.isSyncing = true;
@@ -51,10 +58,6 @@ class SyncManager {
51
 
52
  if (error) throw error;
53
 
54
- // We merge remote into local if remote updated_at > local, but sqlite doesn't have updated_at for settings.
55
- // For now, we prefer local over remote if local has a valid API key, otherwise pull remote.
56
- // Easiest sync strategy: Push all local to remote if local exists, else pull from remote.
57
-
58
  const remoteSettingsMap = {};
59
  if (remoteSettings) {
60
  for (const row of remoteSettings) {
@@ -62,6 +65,23 @@ class SyncManager {
62
  }
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  for (const row of localSettings) {
66
  const encryptedValue = await cryptoService.encrypt(row.value);
67
  await supabase.from('settings').upsert({
 
19
  });
20
  }
21
 
22
+ async initialize() {
23
+ // Called once on app startup to trigger initial sync if already logged in
24
+ if (authService.user && authService.isCryptoUnlocked()) {
25
+ this.sync();
26
+ }
27
+ }
28
+
29
  async sync() {
30
  if (!authService.user || !authService.isCryptoUnlocked() || this.isSyncing) return;
31
  this.isSyncing = true;
 
58
 
59
  if (error) throw error;
60
 
 
 
 
 
61
  const remoteSettingsMap = {};
62
  if (remoteSettings) {
63
  for (const row of remoteSettings) {
 
65
  }
66
  }
67
 
68
+ // Check if local settings is effectively empty (only defaults, no api keys)
69
+ const hasLocalApiKey = localSettings.some(r => r.key.includes('api_key') && r.value && r.value.trim().length > 0);
70
+ const hasRemoteApiKey = Object.keys(remoteSettingsMap).some(k => k.includes('api_key') && remoteSettingsMap[k] && remoteSettingsMap[k].trim().length > 0);
71
+
72
+ // If local has no API keys, but remote has some, we assume this is a NEW login on a new device.
73
+ // In this case, we DO NOT push our local defaults to remote. We just pull from remote.
74
+ if (!hasLocalApiKey && hasRemoteApiKey) {
75
+ for (const key of Object.keys(remoteSettingsMap)) {
76
+ if (remoteSettingsMap[key]) {
77
+ db.setSetting(key, remoteSettingsMap[key]);
78
+ }
79
+ }
80
+ return; // Skip pushing to remote
81
+ }
82
+
83
+ // Otherwise, push local to remote
84
+
85
  for (const row of localSettings) {
86
  const encryptedValue = await cryptoService.encrypt(row.value);
87
  await supabase.from('settings').upsert({
vite.config.js CHANGED
@@ -30,7 +30,7 @@ export default defineConfig({
30
  VitePWA({
31
  registerType: 'autoUpdate',
32
  devOptions: {
33
- enabled: true
34
  },
35
  workbox: {
36
  skipWaiting: true,
 
30
  VitePWA({
31
  registerType: 'autoUpdate',
32
  devOptions: {
33
+ enabled: false
34
  },
35
  workbox: {
36
  skipWaiting: true,