fix: deploy error visibility, trap cleanup, variable quoting, frontend resilience
- S10: Add warnings to silent health check failures in deploy scripts - S11: Add trap cleanup for temp dirs in deploy and tailscale scripts - S12: Quote 20+ critical unquoted variables across deploy scripts - S13: Extract hardcoded IPs to deploy-config-defaults.sh - S15: Add --memory=256m to UI container runs - F16: Remove in-memory JWT, use cookie-only auth in filebrowser client - F17: Add meta tag fallback for CSRF token in RPC client - F19: Track and clear setTimeout in AppSession on unmount Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ export function sanitizePath(path: string): string {
|
||||
}
|
||||
|
||||
class FileBrowserClient {
|
||||
private token: string | null = null
|
||||
private _authenticated = false
|
||||
private baseUrl: string
|
||||
|
||||
constructor() {
|
||||
@@ -44,7 +44,12 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
return this.token !== null
|
||||
return this._authenticated
|
||||
}
|
||||
|
||||
private getAuthCookie(): string | null {
|
||||
const match = document.cookie.match(/(?:^|;\s*)auth=([^;]+)/)
|
||||
return match ? match[1]! : null
|
||||
}
|
||||
|
||||
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
||||
@@ -57,10 +62,11 @@ class FileBrowserClient {
|
||||
if (!res.ok) return false
|
||||
const text = await res.text()
|
||||
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
||||
this.token = text.replace(/^"|"$/g, '')
|
||||
// Store token as cookie for img/video/audio src requests (avoids token in URL)
|
||||
const token = text.replace(/^"|"$/g, '')
|
||||
// Store token as cookie — the only auth mechanism we use
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||
document.cookie = `auth=${this.token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
|
||||
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
|
||||
this._authenticated = true
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
@@ -69,13 +75,14 @@ class FileBrowserClient {
|
||||
|
||||
private headers(): Record<string, string> {
|
||||
const h: Record<string, string> = {}
|
||||
if (this.token) h['X-Auth'] = this.token
|
||||
const cookie = this.getAuthCookie()
|
||||
if (cookie) h['X-Auth'] = cookie
|
||||
return h
|
||||
}
|
||||
|
||||
/** Ensure we're authenticated before making a request. Auto-logins if needed. */
|
||||
private async ensureAuth(): Promise<void> {
|
||||
if (this.token) return
|
||||
if (this._authenticated && this.getAuthCookie()) return
|
||||
const ok = await this.login()
|
||||
if (!ok) throw new Error('FileBrowser authentication failed — please open Cloud to log in')
|
||||
}
|
||||
@@ -175,7 +182,7 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
async getUsage(): Promise<{ totalSize: number; folderCount: number; fileCount: number }> {
|
||||
if (!this.isAuthenticated) {
|
||||
if (!this._authenticated || !this.getAuthCookie()) {
|
||||
const ok = await this.login()
|
||||
if (!ok) return { totalSize: 0, folderCount: 0, fileCount: 0 }
|
||||
}
|
||||
@@ -205,7 +212,7 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
async readFileAsText(path: string, maxBytes = 102400): Promise<{ content: string; truncated: boolean; size: number }> {
|
||||
if (!this.isAuthenticated) {
|
||||
if (!this._authenticated || !this.getAuthCookie()) {
|
||||
const ok = await this.login()
|
||||
if (!ok) throw new Error('FileBrowser authentication failed')
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export interface RPCResponse<T> {
|
||||
|
||||
function getCsrfToken(): string | null {
|
||||
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
|
||||
return match ? match[1]! : null
|
||||
if (match) return match[1]!
|
||||
// Fallback: check for a meta tag (useful when cookies are blocked or not yet set)
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? null
|
||||
}
|
||||
|
||||
class RPCClient {
|
||||
|
||||
@@ -226,6 +226,7 @@ const showModeMenu = ref(false)
|
||||
const autoRetryCount = ref(0)
|
||||
let loadTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let autoRetryId: ReturnType<typeof setTimeout> | null = null
|
||||
let iframeCheckId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** Sites known to block iframes — skip the timeout and go straight to fallback */
|
||||
const IFRAME_BLOCKED_APPS = new Set<string>([])
|
||||
@@ -504,7 +505,7 @@ function onLoad() {
|
||||
isRefreshing.value = false
|
||||
autoRetryCount.value = 0
|
||||
// Check if iframe actually loaded content (same-origin only)
|
||||
setTimeout(() => {
|
||||
iframeCheckId = setTimeout(() => {
|
||||
try {
|
||||
const doc = iframeRef.value?.contentDocument
|
||||
if (doc) {
|
||||
@@ -699,6 +700,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
if (loadTimeoutId) clearTimeout(loadTimeoutId)
|
||||
if (autoRetryId) clearTimeout(autoRetryId)
|
||||
if (iframeCheckId) clearTimeout(iframeCheckId)
|
||||
window.removeEventListener('keydown', onKeyDown, true)
|
||||
window.removeEventListener('message', onMessage)
|
||||
document.removeEventListener('click', onClickOutside)
|
||||
|
||||
Reference in New Issue
Block a user