Skip to content

Commit b36ef2f

Browse files
elemdosclaude
andcommitted
fix: handle server auth token expiry gracefully
- Store admin password in setup file for re-authentication - Add /api/auth/save-server-credentials endpoint - Save credentials on login for server-side auth recovery - Redirect to login on server_auth_expired error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2a22417 commit b36ef2f

File tree

6 files changed

+135
-17
lines changed

6 files changed

+135
-17
lines changed

src/lib/server/pb.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function getAuthConfig(): { token: string } | { email: string; password: string
3434
return { token: setup.auth_token }
3535
}
3636

37-
// Fall back to password (legacy format) - will migrate to token on success
37+
// Fall back to password
3838
if (setup.admin_email && setup.admin_password) {
3939
return { email: setup.admin_email, password: setup.admin_password }
4040
}
@@ -53,6 +53,31 @@ function getAuthConfig(): { token: string } | { email: string; password: string
5353
return null
5454
}
5555

56+
// Get password credentials for re-auth when token expires
57+
function getPasswordCredentials(): { email: string; password: string } | null {
58+
// Try setup file first
59+
try {
60+
if (fs.existsSync(SETUP_FILE)) {
61+
const data = fs.readFileSync(SETUP_FILE, 'utf-8')
62+
const setup = JSON.parse(data)
63+
if (setup.admin_email && setup.admin_password) {
64+
return { email: setup.admin_email, password: setup.admin_password }
65+
}
66+
}
67+
} catch {
68+
// Fall through
69+
}
70+
71+
// Try env vars
72+
const email = env.POCKETBASE_ADMIN_EMAIL
73+
const password = env.POCKETBASE_ADMIN_PASSWORD
74+
if (email && password) {
75+
return { email, password }
76+
}
77+
78+
return null
79+
}
80+
5681
export async function ensureAuth(): Promise<boolean> {
5782
// If already authenticated with valid token, return true
5883
if (isAuthenticated && pb.authStore.isValid) {
@@ -93,9 +118,25 @@ export async function ensureAuth(): Promise<boolean> {
93118
saveRefreshedToken(pb.authStore.token)
94119
return true
95120
} catch {
96-
// Token invalid/expired, can't refresh without password
97-
console.warn('[PB] Saved token invalid, need fresh setup or env vars')
121+
// Token invalid/expired - try password fallback from setup file or env vars
122+
console.warn('[PB] Saved token expired, trying password fallback...')
98123
pb.authStore.clear()
124+
125+
// Try setup file password first, then env vars
126+
const creds = getPasswordCredentials()
127+
if (creds) {
128+
try {
129+
await pb.collection('_superusers').authWithPassword(creds.email, creds.password)
130+
isAuthenticated = true
131+
console.log('[PB] Server re-authenticated via password (token was expired)')
132+
saveRefreshedToken(pb.authStore.token)
133+
return true
134+
} catch (e: any) {
135+
console.error('[PB] Password auth failed:', e.message || e)
136+
}
137+
}
138+
139+
console.error('[PB] Auth token expired and no password available. Re-run /setup')
99140
return false
100141
}
101142
} else {
@@ -115,23 +156,17 @@ export async function ensureAuth(): Promise<boolean> {
115156
}
116157
}
117158

118-
// Save refreshed token back to setup file (and remove password if present)
159+
// Save refreshed token back to setup file (keeps password for re-auth fallback)
119160
function saveRefreshedToken(token: string): void {
120161
try {
121162
if (!fs.existsSync(SETUP_FILE)) return
122163

123164
const data = fs.readFileSync(SETUP_FILE, 'utf-8')
124165
const setup = JSON.parse(data)
125166

126-
// Update token
167+
// Update token (password stays for re-auth if token expires)
127168
setup.auth_token = token
128169

129-
// Remove password if present (migration from legacy format)
130-
if (setup.admin_password) {
131-
delete setup.admin_password
132-
console.log('[PB] Migrated from password to token-based auth')
133-
}
134-
135170
fs.writeFileSync(SETUP_FILE, JSON.stringify(setup), { mode: 0o600 })
136171
} catch {
137172
// Silently fail - not critical
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { json } from '@sveltejs/kit'
2+
import type { RequestHandler } from './$types'
3+
import PocketBase from 'pocketbase'
4+
import * as fs from 'fs/promises'
5+
6+
const PB_URL = process.env.POCKETBASE_URL || 'http://127.0.0.1:8091'
7+
const SETUP_FILE = './pocketbase/pb_data/.setup_complete'
8+
9+
/**
10+
* Save server credentials after user login
11+
* This allows the server to re-authenticate when tokens expire
12+
*/
13+
export const POST: RequestHandler = async ({ request }) => {
14+
try {
15+
const { email, password } = await request.json()
16+
17+
if (!email || !password) {
18+
return json({ error: 'Email and password required' }, { status: 400 })
19+
}
20+
21+
// Verify credentials work with PocketBase superusers
22+
const pb = new PocketBase(PB_URL)
23+
try {
24+
await pb.collection('_superusers').authWithPassword(email, password)
25+
} catch (e: any) {
26+
// Not a superuser or wrong password - that's fine, they might just be a regular user
27+
// Only superusers can save server credentials
28+
return json({ ok: true, saved: false })
29+
}
30+
31+
// Credentials are valid for superuser - save them
32+
try {
33+
let setup: any = {}
34+
35+
// Try to read existing setup file
36+
try {
37+
const data = await fs.readFile(SETUP_FILE, 'utf-8')
38+
setup = JSON.parse(data)
39+
} catch {
40+
// File doesn't exist or invalid - start fresh
41+
}
42+
43+
// Update with new credentials
44+
setup.admin_email = email
45+
setup.admin_password = password
46+
setup.auth_token = pb.authStore.token
47+
setup.timestamp = new Date().toISOString()
48+
49+
await fs.writeFile(SETUP_FILE, JSON.stringify(setup), { mode: 0o600 })
50+
51+
return json({ ok: true, saved: true })
52+
} catch (e: any) {
53+
console.error('[Auth] Failed to save server credentials:', e.message)
54+
return json({ ok: true, saved: false })
55+
}
56+
} catch (e: any) {
57+
console.error('[Auth] Error:', e.message)
58+
return json({ error: 'Failed to process request' }, { status: 500 })
59+
}
60+
}

src/routes/api/settings/+server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const GET: RequestHandler = async ({ url, request }) => {
5959

6060
// Ensure server pb client is authenticated
6161
if (!await ensureAuth()) {
62-
return json({ error: 'Server not configured' }, { status: 500 })
62+
return json({ error: 'server_auth_expired', message: 'Server authentication expired. Please log in again.' }, { status: 503 })
6363
}
6464

6565
try {
@@ -94,7 +94,7 @@ export const POST: RequestHandler = async ({ request }) => {
9494

9595
// Ensure server pb client is authenticated
9696
if (!await ensureAuth()) {
97-
return json({ error: 'Server not configured' }, { status: 500 })
97+
return json({ error: 'server_auth_expired', message: 'Server authentication expired. Please log in again.' }, { status: 503 })
9898
}
9999

100100
try {

src/routes/api/setup/+server.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,19 +156,21 @@ export const POST: RequestHandler = async ({ request }) => {
156156
}
157157
}
158158

159-
// 7. Save auth token for server-side operations
160-
// We save the token (not password) - it can be refreshed by the server
159+
// 7. Save auth credentials for server-side operations
160+
// We save both token (for fast auth) and password (for re-auth if token expires)
161+
// File is protected with 0o600 permissions and lives in pb_data which contains the DB anyway
161162
try {
162163
const fs = await import("fs/promises")
163164
const setup_data = JSON.stringify({
164165
timestamp: new Date().toISOString(),
165166
admin_email: email,
167+
admin_password: password,
166168
auth_token: pb.authStore.token
167169
})
168170
await fs.writeFile("./pocketbase/pb_data/.setup_complete", setup_data, { mode: 0o600 })
169-
console.log("[Setup] Auth token saved")
171+
console.log("[Setup] Auth credentials saved")
170172
} catch (err: any) {
171-
console.error("[Setup] Failed to save auth token:", err.message)
173+
console.error("[Setup] Failed to save auth credentials:", err.message)
172174
// Non-fatal
173175
}
174176

src/routes/login/+page.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
3434
try {
3535
await auth.login(email, password)
36+
37+
// Also save credentials for server-side auth (handles token expiry)
38+
// This is fire-and-forget - don't block login on it
39+
fetch("/api/auth/save-server-credentials", {
40+
method: "POST",
41+
headers: { "Content-Type": "application/json" },
42+
body: JSON.stringify({ email, password })
43+
}).catch(() => {})
44+
3645
goto("/tinykit")
3746
} catch (err: any) {
3847
error = err.message || "Login failed. Please check your credentials."

src/routes/tinykit/settings/+page.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { onMount } from "svelte";
3+
import { goto } from "$app/navigation";
34
import {
45
ArrowLeft,
56
Loader2,
@@ -14,6 +15,15 @@
1415
import { get_saved_theme, apply_builder_theme } from "$lib/builder_themes";
1516
import { pb } from "$lib/pocketbase.svelte";
1617
18+
// Handle server auth expiry - redirect to login
19+
function handle_api_error(data: any, status: number): boolean {
20+
if (status === 503 && data.error === "server_auth_expired") {
21+
goto("/login")
22+
return true
23+
}
24+
return false
25+
}
26+
1727
interface LLMConfig {
1828
provider: string;
1929
api_key: string;
@@ -106,6 +116,7 @@
106116
},
107117
});
108118
const data = await res.json();
119+
if (handle_api_error(data, res.status)) return;
109120
if (data.value) {
110121
// Store masked key separately, clear the input field
111122
masked_api_key = data.value.api_key || "";
@@ -150,6 +161,7 @@
150161
});
151162
152163
const data = await res.json();
164+
if (handle_api_error(data, res.status)) return;
153165
if (!res.ok) {
154166
throw new Error(data.error || "Failed to save settings");
155167
}

0 commit comments

Comments
 (0)