From b4d9e3dd5fed1ec405631ddb7e33e1758d06d3b6 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sun, 19 Apr 2026 21:48:13 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20useAuthFetch-hook=20f=C3=B6r=20auto?= =?UTF-8?q?matisk=20JWT-header=20i=20klientanrop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/recipes/[id]/RecipeDetailClient.tsx | 14 ++++----- .../app/recipes/import/ImportRecipePage.tsx | 7 ++--- .../app/recipes/write/WriteRecipePage.tsx | 7 ++--- frontend/lib/use-auth-fetch.ts | 29 +++++++++++++++++++ 4 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 frontend/lib/use-auth-fetch.ts diff --git a/frontend/app/recipes/[id]/RecipeDetailClient.tsx b/frontend/app/recipes/[id]/RecipeDetailClient.tsx index 58f6e32a..51bafb21 100644 --- a/frontend/app/recipes/[id]/RecipeDetailClient.tsx +++ b/frontend/app/recipes/[id]/RecipeDetailClient.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useTransition } from 'react'; import { useRouter } from 'next/navigation'; -import { useSession } from 'next-auth/react'; +import { useAuthFetch } from '../../../lib/use-auth-fetch'; import type { Recipe, Product, @@ -65,7 +65,7 @@ function StatusBadge({ status }: { status: 'enough' | 'missing' | 'unit_mismatch export default function RecipeDetailClient({ recipe: initialRecipe }: { recipe: Recipe }) { const router = useRouter(); - const { data: session } = useSession(); + const authFetch = useAuthFetch(); const [recipe, setRecipe] = useState(initialRecipe); const [isEditing, setIsEditing] = useState(false); const [isLiked, setIsLiked] = useState(false); @@ -142,9 +142,7 @@ export default function RecipeDetailClient({ recipe: initialRecipe }: { recipe: if (!confirm(`Ta bort receptet "${recipe.name}"? Det går inte att ångra.`)) return; setIsDeleting(true); try { - const res = await fetch(`/api/recipes/${recipe.id}`, { method: 'DELETE', - headers: { Authorization: `Bearer ${session?.accessToken}` }, - }); + const res = await authFetch(`/api/recipes/${recipe.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(await parseErrorResponse(res)); router.push('/recipes'); } catch (err) { @@ -166,9 +164,8 @@ export default function RecipeDetailClient({ recipe: initialRecipe }: { recipe: quantity: Number(ing.quantity), })), }; - const res = await fetch(`/api/recipes/${recipe.id}`, { + const res = await authFetch(`/api/recipes/${recipe.id}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session?.accessToken}` }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await parseErrorResponse(res)); @@ -188,9 +185,8 @@ export default function RecipeDetailClient({ recipe: initialRecipe }: { recipe: setIsUploadingImage(true); setImageError(null); try { - const res = await fetch(`/api/recipes/${recipe.id}/image`, { + const res = await authFetch(`/api/recipes/${recipe.id}/image`, { method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session?.accessToken}` }, body: JSON.stringify({ sourceUrl: imageUrlInput.trim() }), }); if (!res.ok) throw new Error(await parseErrorResponse(res)); diff --git a/frontend/app/recipes/import/ImportRecipePage.tsx b/frontend/app/recipes/import/ImportRecipePage.tsx index b684e29d..9971c8a9 100644 --- a/frontend/app/recipes/import/ImportRecipePage.tsx +++ b/frontend/app/recipes/import/ImportRecipePage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { useSession } from 'next-auth/react'; +import { useAuthFetch } from '../../../lib/use-auth-fetch'; import { fetchJson } from '../../../lib/api'; import { parseErrorResponse } from '../../../lib/error-handler'; import type { Product } from '../../../features/inventory/types'; @@ -38,7 +38,7 @@ type Step = 'input' | 'review' | 'saving'; export default function ImportRecipePage() { const router = useRouter(); - const { data: session } = useSession(); + const authFetch = useAuthFetch(); const [step, setStep] = useState('input'); const [markdown, setMarkdown] = useState(''); const [parsed, setParsed] = useState(null); @@ -136,9 +136,8 @@ export default function ImportRecipePage() { }; try { - const res = await fetch('/api/recipes', { + const res = await authFetch('/api/recipes', { method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session?.accessToken}` }, body: JSON.stringify(body), }); diff --git a/frontend/app/recipes/write/WriteRecipePage.tsx b/frontend/app/recipes/write/WriteRecipePage.tsx index 112c9cdf..60ecb8b1 100644 --- a/frontend/app/recipes/write/WriteRecipePage.tsx +++ b/frontend/app/recipes/write/WriteRecipePage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { useSession } from 'next-auth/react'; +import { useAuthFetch } from '../../../lib/use-auth-fetch'; import { fetchJson } from '../../../lib/api'; import { parseErrorResponse } from '../../../lib/error-handler'; import type { Product } from '../../../features/inventory/types'; @@ -37,7 +37,7 @@ type Step = 'input' | 'review' | 'saving' | 'saved'; export default function WriteRecipePage() { const router = useRouter(); - const { data: session } = useSession(); + const authFetch = useAuthFetch(); const [step, setStep] = useState('input'); const [markdown, setMarkdown] = useState(''); const [parsed, setParsed] = useState(null); @@ -193,9 +193,8 @@ export default function WriteRecipePage() { }; try { - const res = await fetch('/api/recipes', { + const res = await authFetch('/api/recipes', { method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session?.accessToken}` }, body: JSON.stringify(body), }); diff --git a/frontend/lib/use-auth-fetch.ts b/frontend/lib/use-auth-fetch.ts new file mode 100644 index 00000000..ee48b8a1 --- /dev/null +++ b/frontend/lib/use-auth-fetch.ts @@ -0,0 +1,29 @@ +'use client'; + +import { useSession } from 'next-auth/react'; +import { useCallback } from 'react'; + +/** + * Hook som returnerar en fetch-funktion med Authorization-header automatiskt ifylld. + * Används i klientkomponenter som gör anrop till endpoints som Caddy routar direkt + * till NestJS (t.ex. /api/recipes*, /api/products*, /api/inventory*). + * + * Exempel: + * const authFetch = useAuthFetch(); + * const res = await authFetch('/api/recipes/1', { method: 'PATCH', body: JSON.stringify(data) }); + */ +export function useAuthFetch() { + const { data: session } = useSession(); + + return useCallback( + (url: string, init: RequestInit = {}): Promise => { + const headers = new Headers(init.headers); + headers.set('Authorization', `Bearer ${session?.accessToken ?? ''}`); + if (!headers.has('Content-Type') && init.body && typeof init.body === 'string') { + headers.set('Content-Type', 'application/json'); + } + return fetch(url, { ...init, headers }); + }, + [session?.accessToken], + ); +}