Skip to content

Commit c176398

Browse files
committed
auth fixes
1 parent 83c10e2 commit c176398

File tree

2 files changed

+111
-63
lines changed

2 files changed

+111
-63
lines changed

frontend/src/routes/Editor.svelte

Lines changed: 84 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@
2424
lang?: string;
2525
lang_version?: string;
2626
}
27+
28+
// API error response type (FastAPI returns {detail: string} or {detail: ValidationError[]})
29+
interface ApiErrorResponse {
30+
detail?: string | Array<{ loc: (string | number)[]; msg: string; type: string }>;
31+
}
32+
33+
function getErrorMessage(err: unknown, fallback: string): string {
34+
if (err && typeof err === 'object' && 'detail' in err) {
35+
const detail = (err as ApiErrorResponse).detail;
36+
if (typeof detail === 'string') return detail;
37+
if (Array.isArray(detail) && detail.length > 0) {
38+
return detail.map(e => e.msg).join(', ');
39+
}
40+
}
41+
if (err instanceof Error) return err.message;
42+
return fallback;
43+
}
2744
import {addToast} from "../stores/toastStore";
2845
import Spinner from "../components/Spinner.svelte";
2946
import {goto} from "@mateothegreat/svelte5-router";
@@ -572,31 +589,30 @@
572589
}
573590
574591
} catch (err) {
575-
apiError = err.response?.data?.detail || "Error initiating script execution.";
592+
apiError = getErrorMessage(err, "Error initiating script execution.");
576593
addToast(apiError, "error");
577594
result = {status: 'error', errors: apiError, execution_id: executionId};
578-
console.error("Error executing script:", err.response || err);
595+
console.error("Error executing script:", err);
579596
} finally {
580597
executing = false;
581598
}
582599
}
583600
584601
async function loadSavedScripts() {
585602
if (!authenticated) return;
586-
try {
587-
const { data, error } = await listSavedScriptsApiV1ScriptsGet({});
588-
if (error) throw error;
589-
savedScripts = (data || []).map((script, index) => ({
590-
...script,
591-
id: script.id || script._id || `temp_${index}_${Date.now()}`
592-
}));
593-
} catch (err) {
594-
console.error("Error loading saved scripts:", err);
603+
const { data, error, response } = await listSavedScriptsApiV1ScriptsGet({});
604+
if (error) {
605+
console.error("Error loading saved scripts:", error);
595606
addToast("Failed to load saved scripts. You might need to log in again.", "error");
596-
if (err?.status === 401) {
607+
if (response?.status === 401) {
597608
handleLogout();
598609
}
610+
return;
599611
}
612+
savedScripts = (data || []).map((script, index) => ({
613+
...script,
614+
id: script.id || script._id || `temp_${index}_${Date.now()}`
615+
}));
600616
}
601617
602618
function loadScript(scriptData: EditorScriptData): void {
@@ -644,48 +660,57 @@
644660
const currentIdValue = get(currentScriptId);
645661
let operation = currentIdValue ? 'update' : 'create';
646662
647-
try {
648-
const scriptData = {
649-
name: nameValue,
650-
script: scriptValue,
651-
lang: langValue,
652-
lang_version: versionValue
653-
};
654-
655-
if (operation === 'update') {
656-
const { error: updateErr } = await updateSavedScriptApiV1ScriptsScriptIdPut({
657-
path: { script_id: currentIdValue },
658-
body: scriptData
659-
});
660-
if (updateErr) {
661-
if (updateErr?.status === 404) {
662-
console.log('Script not found, falling back to create operation');
663-
currentScriptId.set(null);
664-
operation = 'create';
665-
const { data, error } = await createSavedScriptApiV1ScriptsPost({ body: scriptData });
666-
if (error) throw error;
667-
currentScriptId.set(data.id);
668-
addToast("Script saved successfully.", "success");
669-
} else {
670-
throw updateErr;
663+
const scriptData = {
664+
name: nameValue,
665+
script: scriptValue,
666+
lang: langValue,
667+
lang_version: versionValue
668+
};
669+
670+
if (operation === 'update') {
671+
const { error: updateErr, response: updateResp } = await updateSavedScriptApiV1ScriptsScriptIdPut({
672+
path: { script_id: currentIdValue },
673+
body: scriptData
674+
});
675+
if (updateErr) {
676+
if (updateResp?.status === 404) {
677+
console.log('Script not found, falling back to create operation');
678+
currentScriptId.set(null);
679+
operation = 'create';
680+
const { data, error, response } = await createSavedScriptApiV1ScriptsPost({ body: scriptData });
681+
if (error) {
682+
console.error('Error saving script:', error);
683+
addToast('Failed to save script. Please try again.', 'error');
684+
if (response?.status === 401) handleLogout();
685+
return;
671686
}
687+
currentScriptId.set(data.id);
688+
addToast("Script saved successfully.", "success");
689+
} else if (updateResp?.status === 401) {
690+
console.error('Error updating script:', updateErr);
691+
addToast('Failed to update script. Please try again.', 'error');
692+
handleLogout();
693+
return;
672694
} else {
673-
addToast("Script updated successfully.", "success");
695+
console.error('Error updating script:', updateErr);
696+
addToast('Failed to update script. Please try again.', 'error');
697+
return;
674698
}
675699
} else {
676-
const { data, error } = await createSavedScriptApiV1ScriptsPost({ body: scriptData });
677-
if (error) throw error;
678-
currentScriptId.set(data.id);
679-
addToast("Script saved successfully.", "success");
700+
addToast("Script updated successfully.", "success");
680701
}
681-
await loadSavedScripts();
682-
} catch (err) {
683-
console.error(`Error ${operation === 'update' ? 'updating' : 'saving'} script:`, err);
684-
addToast(`Failed to ${operation} script. Please try again.`, "error");
685-
if (err?.status === 401) {
686-
handleLogout();
702+
} else {
703+
const { data, error, response } = await createSavedScriptApiV1ScriptsPost({ body: scriptData });
704+
if (error) {
705+
console.error('Error saving script:', error);
706+
addToast('Failed to save script. Please try again.', 'error');
707+
if (response?.status === 401) handleLogout();
708+
return;
687709
}
710+
currentScriptId.set(data.id);
711+
addToast("Script saved successfully.", "success");
688712
}
713+
await loadSavedScripts();
689714
}
690715
691716
async function deleteScript(scriptIdToDelete: string): Promise<void> {
@@ -697,23 +722,22 @@
697722
698723
if (!confirm(confirmMessage)) return;
699724
700-
try {
701-
const { error } = await deleteSavedScriptApiV1ScriptsScriptIdDelete({
702-
path: { script_id: scriptIdToDelete }
703-
});
704-
if (error) throw error;
705-
addToast("Script deleted successfully.", "success");
706-
if (get(currentScriptId) === scriptIdToDelete) {
707-
newScript();
708-
}
709-
await loadSavedScripts();
710-
} catch (err) {
711-
console.error("Error deleting script:", err);
725+
const { error, response } = await deleteSavedScriptApiV1ScriptsScriptIdDelete({
726+
path: { script_id: scriptIdToDelete }
727+
});
728+
if (error) {
729+
console.error("Error deleting script:", error);
712730
addToast("Failed to delete script.", "error");
713-
if (err?.status === 401) {
731+
if (response?.status === 401) {
714732
handleLogout();
715733
}
734+
return;
735+
}
736+
addToast("Script deleted successfully.", "success");
737+
if (get(currentScriptId) === scriptIdToDelete) {
738+
newScript();
716739
}
740+
await loadSavedScripts();
717741
}
718742
719743
function newScript() {

frontend/src/stores/auth.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export async function login(user: string, password: string): Promise<boolean> {
8484
});
8585

8686
authCache = { valid: true, timestamp: Date.now() };
87-
try { await fetchUserProfile(); } catch {}
87+
try {
88+
await fetchUserProfile();
89+
} catch (err) {
90+
console.warn('Failed to fetch user profile after login:', err);
91+
}
8892
return true;
8993
}
9094

@@ -109,6 +113,19 @@ export async function logout(): Promise<void> {
109113
}
110114
}
111115

116+
/**
117+
* Verifies the current authentication state with the server.
118+
*
119+
* OFFLINE-FIRST BEHAVIOR: On network failure, this function returns the cached
120+
* auth state (if available) rather than immediately logging the user out.
121+
* This provides better UX during transient network issues but means:
122+
* - Server-revoked tokens may remain "valid" locally for up to AUTH_CACHE_DURATION (30s)
123+
* - Security-critical operations should use forceRefresh=true
124+
*
125+
* Trade-off: We prioritize availability over immediate consistency for better
126+
* offline/flaky-network UX. The 30-second cache window is acceptable for most
127+
* UI operations; sensitive actions should force re-verification.
128+
*/
112129
export async function verifyAuth(forceRefresh = false): Promise<boolean> {
113130
if (!forceRefresh && authCache.valid !== null && Date.now() - authCache.timestamp < AUTH_CACHE_DURATION) {
114131
return authCache.valid;
@@ -136,9 +153,16 @@ export async function verifyAuth(forceRefresh = false): Promise<boolean> {
136153
userEmail: null
137154
});
138155
authCache = { valid: true, timestamp: Date.now() };
139-
try { await fetchUserProfile(); } catch {}
156+
try {
157+
await fetchUserProfile();
158+
} catch (err) {
159+
console.warn('Failed to fetch user profile during verification:', err);
160+
}
140161
return true;
141-
} catch {
162+
} catch (err) {
163+
// Network error - use cached state if available (offline-first)
164+
// See function docstring for security trade-off explanation
165+
console.warn('Auth verification failed (network error):', err);
142166
if (authCache.valid !== null) return authCache.valid;
143167
clearAuth();
144168
return false;

0 commit comments

Comments
 (0)