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
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
apiGetFileDetails,
formatHeaders,
getFileDownloadUrl,
getFileThumbnailUrl,
type UserFileDetail,
@@ -57,11 +58,10 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
? allFiles.filter((f) => checkedIds.has(f.id))
: [{ id: details.id, originalName: details.originalName, mimeType: details.mimeType }];
const token = localStorage.getItem("stirling-token") || "";
const downloaded = await Promise.all(
filesToOpen.map(async (f) => {
const res = await fetch(getFileDownloadUrl(f.id), {
headers: { Authorization: `Bearer ${token}` },
headers: formatHeaders(),
});
if (!res.ok) return null;
const blob = await res.blob();
@@ -23,7 +23,7 @@ import {
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { apiDelete, apiGet, apiPost, apiPut, clearToken } from "@/lib/api";
import { apiDelete, apiGet, apiPost, apiPut, clearToken, formatHeaders } from "@/lib/api";
import { cn, copyToClipboard } from "@/lib/utils";
import { GemLogo } from "../common/gem-logo";
@@ -287,10 +287,9 @@ function SystemSection() {
const formData = new FormData();
formData.append("file", file);
try {
const token = localStorage.getItem("stirling-token");
await fetch("/api/v1/settings/logo", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
headers: formatHeaders(),
body: formData,
});
setSettings((prev) => ({ ...prev, customLogo: "true" }));
@@ -1,12 +1,8 @@
import { Check, Copy, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { copyToClipboard } from "@/lib/utils";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function BarcodeReadSettings() {
const { files, processing, error, setProcessing, setError } = useFileStore();
const [result, setResult] = useState<{ found: boolean; text: string | null } | null>(null);
@@ -25,7 +21,7 @@ export function BarcodeReadSettings() {
const res = await fetch("/api/v1/tools/barcode-read", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,7 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function BulkRenameSettings() {
const { files, processing, error, setProcessing, setError } = useFileStore();
const [pattern, setPattern] = useState("image-{{index}}");
@@ -28,7 +24,7 @@ export function BulkRenameSettings() {
const res = await fetch("/api/v1/tools/bulk-rename", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,8 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
type Layout = "2x2" | "3x3" | "1x3" | "2x1" | "3x1" | "1x2";
const LAYOUTS: { value: Layout; label: string }[] = [
@@ -43,7 +40,7 @@ export function CollageSettings() {
const res = await fetch("/api/v1/tools/collage", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,12 +1,8 @@
import { Check, Copy, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { copyToClipboard } from "@/lib/utils";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function ColorPaletteSettings() {
const { files, processing, error, setProcessing, setError } = useFileStore();
const [colors, setColors] = useState<string[]>([]);
@@ -25,7 +21,7 @@ export function ColorPaletteSettings() {
const res = await fetch("/api/v1/tools/color-palette", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,7 @@
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function CompareSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl } = useFileStore();
const [secondFile, setSecondFile] = useState<File | null>(null);
@@ -28,7 +24,7 @@ export function CompareSettings() {
const res = await fetch("/api/v1/tools/compare", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,7 @@
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function ComposeSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
@@ -34,7 +30,7 @@ export function ComposeSettings() {
const res = await fetch("/api/v1/tools/compose", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,14 +1,11 @@
import { Download, Redo, Trash2 } from "lucide-react";
import { useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { formatHeaders } from "@/lib/api";
import { generateId } from "@/lib/utils";
import { useFileStore } from "@/stores/file-store";
import type { EraserCanvasRef } from "./eraser-canvas";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
interface EraseObjectSettingsProps {
eraserRef: React.RefObject<EraserCanvasRef | null>;
hasStrokes: boolean;
@@ -114,7 +111,9 @@ export function EraseObjectSettings({
setProgressPhase("idle");
};
xhr.open("POST", "/api/v1/tools/erase-object");
xhr.setRequestHeader("Authorization", `Bearer ${getToken()}`);
formatHeaders().forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
xhr.send(formData);
};
@@ -1,11 +1,8 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
const SIZES = [
{ name: "favicon-16x16.png", size: "16x16" },
{ name: "favicon-32x32.png", size: "32x32" },
@@ -33,7 +30,7 @@ export function FaviconSettings() {
const res = await fetch("/api/v1/tools/favicon", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,8 @@
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
interface DuplicateGroup {
files: Array<{ filename: string; similarity: number }>;
}
@@ -35,7 +32,7 @@ export function FindDuplicatesSettings() {
const res = await fetch("/api/v1/tools/find-duplicates", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,5 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
const PAGE_SIZES: Record<string, [number, number]> = {
@@ -108,11 +109,6 @@ function PdfPagePreview({
</div>
);
}
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function ImageToPdfSettings() {
const { files, selectedIndex, processing, error, setProcessing, setError } = useFileStore();
const [pageSize, setPageSize] = useState<"A4" | "Letter" | "A3" | "A5">("A4");
@@ -136,7 +132,7 @@ export function ImageToPdfSettings() {
const res = await fetch("/api/v1/tools/image-to-pdf", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,8 @@
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
interface ImageInfoData {
filename: string;
fileSize: number;
@@ -50,7 +47,7 @@ export function InfoSettings() {
const res = await fetch("/api/v1/tools/info", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,13 +1,10 @@
import { Check, Copy } from "lucide-react";
import { useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { formatHeaders } from "@/lib/api";
import { copyToClipboard, generateId } from "@/lib/utils";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
type OcrEngine = "tesseract" | "paddleocr";
const LANGUAGES = [
@@ -112,7 +109,9 @@ export function OcrSettings() {
setProgressPhase("idle");
};
xhr.open("POST", "/api/v1/tools/ocr");
xhr.setRequestHeader("Authorization", `Bearer ${getToken()}`);
formatHeaders().forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
xhr.send(formData);
};
@@ -1,10 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
import { formatHeaders } from "@/lib/api";
export function QrGenerateSettings() {
const [text, setText] = useState("");
const [size, setSize] = useState(400);
@@ -27,10 +23,7 @@ export function QrGenerateSettings() {
try {
const res = await fetch("/api/v1/tools/qr-generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
},
headers: formatHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ text, size, errorCorrection, foreground, background }),
});
@@ -1,11 +1,7 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function SplitSettings() {
const { files, processing, error, setProcessing, setError } = useFileStore();
const [columns, setColumns] = useState(2);
@@ -26,7 +22,7 @@ export function SplitSettings() {
const res = await fetch("/api/v1/tools/split", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -2,12 +2,9 @@ import { AlertTriangle, ChevronDown, ChevronRight, Download, Loader2, MapPin } f
import { useEffect, useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
interface MetadataResult {
filename: string;
fileSize: number;
@@ -342,7 +339,7 @@ export function StripMetadataSettings() {
formData.append("file", currentFile);
const res = await fetch("/api/v1/tools/strip-metadata/inspect", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
signal: controller.signal,
});
@@ -1,11 +1,7 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function SvgToRasterSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
@@ -38,7 +34,7 @@ export function SvgToRasterSettings() {
const res = await fetch("/api/v1/tools/svg-to-raster", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,11 +1,7 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function VectorizeSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
@@ -30,7 +26,7 @@ export function VectorizeSettings() {
const res = await fetch("/api/v1/tools/vectorize", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});
@@ -1,13 +1,9 @@
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
type Position = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function WatermarkImageSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
@@ -35,7 +31,7 @@ export function WatermarkImageSettings() {
const res = await fetch("/api/v1/tools/watermark-image", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
headers: formatHeaders(),
body: formData,
});