fix(web): skip empty Authorization header for forward-auth proxy compatibility

Centralize duplicated getToken() + Bearer header logic into a single
formatHeaders() helper in lib/api.ts. When no token exists, the
Authorization header is omitted entirely instead of sending an empty
Bearer token, which breaks forward-auth proxies like Authelia behind
Caddy.

Changes:
- Add formatHeaders() with try-catch around localStorage access
- Replace 20+ duplicated getToken() definitions across tool components
- Migrate all call sites including file-details, settings, change-password
- Update tests to verify header omission on empty token

Based on the fix proposed by @jules2689 in #6, with improvements:
file placement (lib/api.ts vs components), localStorage error handling,
simplified truthiness check, and complete call-site coverage.

Co-Authored-By: Julian Nadeau <julian@jnadeau.ca>
This commit is contained in:
Siddharth Kumar Sah
2026-04-05 18:41:06 +08:00
co-authored by Julian Nadeau
parent f21579c7a3
commit d0c69d6a46
26 changed files with 104 additions and 177 deletions
+5 -12
View File
@@ -2,6 +2,7 @@ import { Play, Trash2, Workflow } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { AppLayout } from "@/components/layout/app-layout";
import { PipelineBuilder, type PipelineStep } from "@/components/tools/pipeline-builder";
import { formatHeaders } from "@/lib/api";
import { generateId } from "@/lib/utils";
interface SavedPipeline {
@@ -11,11 +12,6 @@ interface SavedPipeline {
steps: Array<{ toolId: string; settings: Record<string, unknown> }>;
createdAt: string;
}
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function AutomatePage() {
const [steps, setSteps] = useState<PipelineStep[]>([]);
const [savedPipelines, setSavedPipelines] = useState<SavedPipeline[]>([]);
@@ -33,7 +29,7 @@ export function AutomatePage() {
const loadPipelines = useCallback(async () => {
try {
const res = await fetch("/api/v1/pipeline/list", {
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
});
if (res.ok) {
const data = await res.json();
@@ -55,10 +51,7 @@ export function AutomatePage() {
try {
const res = await fetch("/api/v1/pipeline/save", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
},
headers: formatHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({
name,
description: description || undefined,
@@ -86,7 +79,7 @@ export function AutomatePage() {
try {
await fetch(`/api/v1/pipeline/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
});
await loadPipelines();
} catch {
@@ -117,7 +110,7 @@ export function AutomatePage() {
const res = await fetch("/api/v1/pipeline/execute", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
+2 -5
View File
@@ -1,4 +1,5 @@
import { type FormEvent, useRef, useState } from "react";
import { formatHeaders } from "@/lib/api";
/**
* Trigger the browser's "Save Password" prompt by submitting a real form
@@ -85,13 +86,9 @@ export function ChangePasswordPage() {
setLoading(true);
try {
const token = localStorage.getItem("stirling-token");
const res = await fetch("/api/auth/change-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
headers: formatHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ currentPassword, newPassword }),
});