Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/lib/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ export class UrlSaveLimitError extends Error {

class ApiClient {
private onUnauthorized: (() => void) | null = null;
private onScopeUpgradeRequired: (() => void) | null = null;

// Set callback for when 401 is received (session invalid)
setOnUnauthorized(callback: () => void) {
this.onUnauthorized = callback;
}

// Set callback for when 403 scope_upgrade_required is received
setOnScopeUpgradeRequired(callback: () => void) {
this.onScopeUpgradeRequired = callback;
}

private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const headers: HeadersInit = {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -78,6 +84,9 @@ class ApiClient {
if (response.status === 403) {
const body = await response.json().catch(() => ({ error: 'Forbidden' }));
if ((body as { error: string }).error === 'scope_upgrade_required') {
if (this.onScopeUpgradeRequired) {
this.onScopeUpgradeRequired();
}
throw new ScopeUpgradeError((body as { message?: string }).message);
}
if ((body as { error: string }).error === 'url_save_limit_reached') {
Expand Down
13 changes: 13 additions & 0 deletions src/lib/stores/auth.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ interface AuthState {
user: User | null;
isLoading: boolean;
error: string | null;
scopeUpgradeRequired: boolean;
}

function createAuthStore() {
let state = $state<AuthState>({
user: null,
isLoading: true,
error: null,
scopeUpgradeRequired: false,
});

// Handle 401 - session expired/invalid on the backend
Expand Down Expand Up @@ -46,6 +48,11 @@ function createAuthStore() {

// Set up 401 handler
api.setOnUnauthorized(handleUnauthorized);

// Set up scope upgrade handler
api.setOnScopeUpgradeRequired(() => {
state.scopeUpgradeRequired = true;
});
}

// Set user after successful authentication
Expand Down Expand Up @@ -114,6 +121,12 @@ function createAuthStore() {
get error() {
return state.error;
},
get scopeUpgradeRequired() {
return state.scopeUpgradeRequired;
},
dismissScopeUpgrade() {
state.scopeUpgradeRequired = false;
},
setUser,
verifySession,
logout,
Expand Down
44 changes: 44 additions & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@
<div class="app">
{#if !auth.isLoading}
{#if auth.isAuthenticated}
{#if auth.scopeUpgradeRequired}
<div class="scope-upgrade-banner">
<span
>Your session was created with outdated permissions. Please <a href="/auth/login"
>log in again</a
> to restore full functionality.</span
>
<button class="dismiss-btn" onclick={() => auth.dismissScopeUpgrade()}>Dismiss</button>
</div>
{/if}
<div class="app-container">
<Sidebar />
<button
Expand Down Expand Up @@ -383,6 +393,40 @@
margin: 0 0.5rem;
}

.scope-upgrade-banner {
background: var(--color-warning-bg, #fff3cd);
color: var(--color-warning-text, #856404);
border-bottom: 1px solid var(--color-warning-border, #ffc107);
padding: 0.625rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
font-size: 0.875rem;
text-align: center;
}

.scope-upgrade-banner a {
color: inherit;
font-weight: 600;
text-decoration: underline;
}

.scope-upgrade-banner .dismiss-btn {
background: none;
border: 1px solid var(--color-warning-text, #856404);
color: inherit;
border-radius: 4px;
padding: 0.25rem 0.5rem;
cursor: pointer;
font-size: 0.75rem;
white-space: nowrap;
}

.scope-upgrade-banner .dismiss-btn:hover {
background: rgba(0, 0, 0, 0.05);
}

@media (max-width: 1000px) {
.app-container {
flex-direction: column;
Expand Down