Spaces:
Running
Running
Savestate: Fix sync settings overwrite bug and persist crypto key in sessionStorage
Browse files- dev-dist/sw.js +1 -1
- dynamischer Name.txt +9 -1
- src/app.js +1 -0
- src/services/auth-service.js +2 -2
- src/services/crypto-service.js +30 -1
- src/services/sync-service.js +24 -4
- vite.config.js +1 -1
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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
-
if (!
|
| 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 |
-
|
| 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:
|
| 34 |
},
|
| 35 |
workbox: {
|
| 36 |
skipWaiting: true,
|
|
|
|
| 30 |
VitePWA({
|
| 31 |
registerType: 'autoUpdate',
|
| 32 |
devOptions: {
|
| 33 |
+
enabled: false
|
| 34 |
},
|
| 35 |
workbox: {
|
| 36 |
skipWaiting: true,
|