diff --git a/frontend/app/api/admin/bulk-categorize/route.ts b/frontend/app/api/admin/bulk-categorize/route.ts index 80c9122a..b3fdfec3 100644 --- a/frontend/app/api/admin/bulk-categorize/route.ts +++ b/frontend/app/api/admin/bulk-categorize/route.ts @@ -1,28 +1,15 @@ -import { auth } from '../../../../auth'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) return {}; - return { Authorization: `Bearer ${session.accessToken}` }; -} - -// POST /api/admin/bulk-categorize -// Body: { productIds?: number[] } -export async function POST(req: Request) { +export const POST = withAuth(async (req, session) => { try { const body = await req.json().catch(() => ({})); const { productIds } = body; - const authHeaders = await getAuthHeaders(); - if (!authHeaders.Authorization) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - const res = await fetch(`${API_BASE}/api/products/ai-categorize-bulk`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify({ productIds }), }); @@ -40,4 +27,4 @@ export async function POST(req: Request) { { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/admin/create-product/route.ts b/frontend/app/api/admin/create-product/route.ts index ea637feb..bd9ee49a 100644 --- a/frontend/app/api/admin/create-product/route.ts +++ b/frontend/app/api/admin/create-product/route.ts @@ -1,16 +1,8 @@ -import { auth } from '../../../../auth'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) { - return {}; - } - return { Authorization: `Bearer ${session.accessToken}` }; -} - -export async function POST(req: Request) { +export const POST = withAuth(async (req, session) => { try { const body = await req.json(); const { name } = body; @@ -19,10 +11,9 @@ export async function POST(req: Request) { return Response.json({ error: 'Name is required' }, { status: 400 }); } - const authHeaders = await getAuthHeaders(); const res = await fetch(`${API_BASE}/api/products`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify({ name }), }); @@ -43,4 +34,4 @@ export async function POST(req: Request) { { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/admin/merge-preview-proxy/route.ts b/frontend/app/api/admin/merge-preview-proxy/route.ts index 35a01c47..4b5080fe 100644 --- a/frontend/app/api/admin/merge-preview-proxy/route.ts +++ b/frontend/app/api/admin/merge-preview-proxy/route.ts @@ -1,29 +1,22 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const sourceProductId = request.nextUrl.searchParams.get('sourceProductId'); - const targetProductId = request.nextUrl.searchParams.get('targetProductId'); +export const GET = withAuth(async (request, session) => { + const { searchParams } = new URL(request.url); + const sourceProductId = searchParams.get('sourceProductId'); + const targetProductId = searchParams.get('targetProductId'); const res = await fetch( `${API_BASE}/api/products/merge-preview?sourceProductId=${sourceProductId}&targetProductId=${targetProductId}`, { - method: 'GET', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }, ); const text = await res.text(); - - return new NextResponse(text, { - status: res.status, - headers: { - 'Content-Type': 'application/json', - }, - }); -} \ No newline at end of file + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); \ No newline at end of file diff --git a/frontend/app/api/admin/product/[id]/route.ts b/frontend/app/api/admin/product/[id]/route.ts index 50a6f62d..d5fd2bb0 100644 --- a/frontend/app/api/admin/product/[id]/route.ts +++ b/frontend/app/api/admin/product/[id]/route.ts @@ -1,23 +1,10 @@ -import { auth } from '../../../../../auth'; +import { withAuth } from '../../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) { - return {}; - } - return { Authorization: `Bearer ${session.accessToken}` }; -} - -// PATCH /api/admin/product/[id] -// Body: { name, canonicalName, category, subcategory, brand, categoryId, tags } -export async function PATCH( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const PATCH = withAuth(async (req, session, context) => { try { - const { id } = await params; + const { id } = await context.params; const productId = Number(id); if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 }); @@ -28,15 +15,11 @@ export async function PATCH( return Response.json({ error: 'Namn får inte vara tomt.' }, { status: 400 }); } - const authHeaders = await getAuthHeaders(); - if (!authHeaders.Authorization) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } + const authHeader = `Bearer ${session.accessToken}`; - // 1. Update product fields const patchRes = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: authHeader }, body: JSON.stringify({ name: name.trim(), canonicalName: canonicalName?.trim() || undefined, @@ -53,10 +36,9 @@ export async function PATCH( return Response.json({ error: `Kunde inte uppdatera produkt: ${text}` }, { status: patchRes.status }); } - // 2. Update tags const tagsRes = await fetch(`${API_BASE}/api/products/${productId}/tags`, { method: 'PUT', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: authHeader }, body: JSON.stringify({ tags: tags ?? [] }), }); @@ -66,17 +48,15 @@ export async function PATCH( return Response.json({ error: `Kunde inte uppdatera taggar: ${text}` }, { status: tagsRes.status }); } - // 3. Return the complete updated product const fullRes = await fetch(`${API_BASE}/api/products/${productId}`, { - headers: authHeaders, + headers: { Authorization: authHeader }, }); if (!fullRes.ok) { return Response.json({ error: 'Produkt uppdaterad men kunde inte hämtas' }, { status: 500 }); } - const product = await fullRes.json(); - return Response.json(product); + return Response.json(await fullRes.json()); } catch (err) { console.error('[api/admin/product] PATCH error:', err); return Response.json( @@ -84,26 +64,17 @@ export async function PATCH( { status: 500 }, ); } -} +}); -// DELETE /api/admin/product/[id] -export async function DELETE( - _req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const DELETE = withAuth(async (_req, session, context) => { try { - const { id } = await params; + const { id } = await context.params; const productId = Number(id); if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 }); - const authHeaders = await getAuthHeaders(); - if (!authHeaders.Authorization) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - const res = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'DELETE', - headers: authHeaders, + headers: { Authorization: `Bearer ${session.accessToken}` }, }); if (!res.ok) { @@ -120,4 +91,5 @@ export async function DELETE( { status: 500 }, ); } -} +}); + diff --git a/frontend/app/api/admin/suggest-category/[id]/route.ts b/frontend/app/api/admin/suggest-category/[id]/route.ts index 077829eb..c21e0f1b 100644 --- a/frontend/app/api/admin/suggest-category/[id]/route.ts +++ b/frontend/app/api/admin/suggest-category/[id]/route.ts @@ -1,30 +1,15 @@ -import { auth } from '../../../../../auth'; +import { withAuth } from '../../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) return {}; - return { Authorization: `Bearer ${session.accessToken}` }; -} - -// GET /api/admin/suggest-category/[id] -export async function GET( - _req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const GET = withAuth(async (_req, session, context) => { try { - const { id } = await params; + const { id } = await context.params; const productId = Number(id); if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 }); - const authHeaders = await getAuthHeaders(); - if (!authHeaders.Authorization) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - const res = await fetch(`${API_BASE}/api/products/${productId}/suggest-category`, { - headers: authHeaders, + headers: { Authorization: `Bearer ${session.accessToken}` }, }); if (!res.ok) { @@ -41,4 +26,4 @@ export async function GET( { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/admin/update-product/[id]/route.ts b/frontend/app/api/admin/update-product/[id]/route.ts index d63dcb5b..9542b7cc 100644 --- a/frontend/app/api/admin/update-product/[id]/route.ts +++ b/frontend/app/api/admin/update-product/[id]/route.ts @@ -1,29 +1,17 @@ -import { auth } from '../../../../../auth'; +import { withAuth } from '../../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) { - return {}; - } - return { Authorization: `Bearer ${session.accessToken}` }; -} - -export async function PATCH( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const PATCH = withAuth(async (req, session, context) => { try { - const { id } = await params; + const { id } = await context.params; const productId = parseInt(id, 10); const body = await req.json(); const { categoryId } = body; - const authHeaders = await getAuthHeaders(); const res = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify({ categoryId }), }); @@ -45,4 +33,4 @@ export async function PATCH( { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/inventory-history-proxy/route.ts b/frontend/app/api/inventory-history-proxy/route.ts index 68fac4d2..19258415 100644 --- a/frontend/app/api/inventory-history-proxy/route.ts +++ b/frontend/app/api/inventory-history-proxy/route.ts @@ -1,25 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const id = request.nextUrl.searchParams.get('id'); +export const GET = withAuth(async (request, session) => { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); const res = await fetch(`${API_BASE}/api/inventory/${id}/consumption-history`, { - method: 'GET', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - - return new NextResponse(text, { - status: res.status, - headers: { - 'Content-Type': 'application/json', - }, - }); -} \ No newline at end of file + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); \ No newline at end of file diff --git a/frontend/app/api/inventory/route.ts b/frontend/app/api/inventory/route.ts index 719f4eda..26310ef8 100644 --- a/frontend/app/api/inventory/route.ts +++ b/frontend/app/api/inventory/route.ts @@ -1,34 +1,26 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const search = request.nextUrl.search; +export const GET = withAuth(async (request, session) => { + const { search } = new URL(request.url); const res = await fetch(`${API_BASE}/api/inventory${search}`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.json(); const res = await fetch(`${API_BASE}/api/inventory`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/meal-plan-proxy/inventory-compare/route.ts b/frontend/app/api/meal-plan-proxy/inventory-compare/route.ts index 077db2e8..1338ba2e 100644 --- a/frontend/app/api/meal-plan-proxy/inventory-compare/route.ts +++ b/frontend/app/api/meal-plan-proxy/inventory-compare/route.ts @@ -1,20 +1,16 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const { searchParams } = request.nextUrl; +export const GET = withAuth(async (request, session) => { + const { searchParams } = new URL(request.url); const from = searchParams.get('from'); const to = searchParams.get('to'); const res = await fetch(`${API_BASE}/api/meal-plan/inventory-compare?from=${from}&to=${to}`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/meal-plan-proxy/route.ts b/frontend/app/api/meal-plan-proxy/route.ts index fdc83a01..7f7a89c7 100644 --- a/frontend/app/api/meal-plan-proxy/route.ts +++ b/frontend/app/api/meal-plan-proxy/route.ts @@ -1,46 +1,35 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const { searchParams } = request.nextUrl; +export const GET = withAuth(async (request, session) => { + const { searchParams } = new URL(request.url); const query = searchParams.toString(); const res = await fetch(`${API_BASE}/api/meal-plan${query ? `?${query}` : ''}`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.text(); const res = await fetch(`${API_BASE}/api/meal-plan`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body, - cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function DELETE(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const date = request.nextUrl.searchParams.get('date'); +export const DELETE = withAuth(async (request, session) => { + const date = new URL(request.url).searchParams.get('date'); const res = await fetch(`${API_BASE}/api/meal-plan/${date}`, { method: 'DELETE', - headers: { ...authHeaders }, - cache: 'no-store', + headers: { Authorization: `Bearer ${session.accessToken}` }, }); return new NextResponse(null, { status: res.status }); -} +}); diff --git a/frontend/app/api/meal-plan-proxy/shopping/route.ts b/frontend/app/api/meal-plan-proxy/shopping/route.ts index 2de22ce8..2ed08415 100644 --- a/frontend/app/api/meal-plan-proxy/shopping/route.ts +++ b/frontend/app/api/meal-plan-proxy/shopping/route.ts @@ -1,20 +1,16 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const { searchParams } = request.nextUrl; +export const GET = withAuth(async (request, session) => { + const { searchParams } = new URL(request.url); const from = searchParams.get('from'); const to = searchParams.get('to'); const res = await fetch(`${API_BASE}/api/meal-plan/shopping-list?from=${from}&to=${to}`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/parse-markdown-proxy/route.ts b/frontend/app/api/parse-markdown-proxy/route.ts index 4c88d38e..beaf94fe 100644 --- a/frontend/app/api/parse-markdown-proxy/route.ts +++ b/frontend/app/api/parse-markdown-proxy/route.ts @@ -1,23 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.text(); const res = await fetch(`${API_BASE}/api/recipes/parse-markdown`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body, cache: 'no-store', }); const text = await res.text(); - - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/products-create/route.ts b/frontend/app/api/products-create/route.ts index 08520df2..0459c3e5 100644 --- a/frontend/app/api/products-create/route.ts +++ b/frontend/app/api/products-create/route.ts @@ -1,16 +1,8 @@ -import { auth } from '../../../auth'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) { - return {}; - } - return { Authorization: `Bearer ${session.accessToken}` }; -} - -export async function POST(req: Request) { +export const POST = withAuth(async (req, session) => { try { const body = await req.json(); const { name } = body; @@ -19,12 +11,9 @@ export async function POST(req: Request) { return Response.json({ error: 'Name is required' }, { status: 400 }); } - const authHeaders = await getAuthHeaders(); - console.log('[products-create] Auth headers:', authHeaders ? 'YES' : 'NO'); - const res = await fetch(`${API_BASE}/api/products`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify({ name }), }); @@ -37,8 +26,6 @@ export async function POST(req: Request) { } const product = await res.json(); - - // Return only serializable fields return Response.json({ id: product.id, name: product.name, @@ -51,4 +38,4 @@ export async function POST(req: Request) { { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/products-update/[id]/route.ts b/frontend/app/api/products-update/[id]/route.ts index 9f553029..ed80889d 100644 --- a/frontend/app/api/products-update/[id]/route.ts +++ b/frontend/app/api/products-update/[id]/route.ts @@ -1,21 +1,10 @@ -import { auth } from '../../../../auth'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -async function getAuthHeaders(): Promise> { - const session = await auth(); - if (!session?.accessToken) { - return {}; - } - return { Authorization: `Bearer ${session.accessToken}` }; -} - -export async function PATCH( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const PATCH = withAuth(async (req, session, context) => { try { - const { id } = await params; + const { id } = await context.params; const productId = parseInt(id, 10); const body = await req.json(); const { categoryId } = body; @@ -24,12 +13,9 @@ export async function PATCH( return Response.json({ error: 'categoryId is required' }, { status: 400 }); } - const authHeaders = await getAuthHeaders(); - console.log('[products-update] Auth headers:', authHeaders ? 'YES' : 'NO'); - const res = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify({ categoryId }), }); @@ -42,8 +28,6 @@ export async function PATCH( } const product = await res.json(); - - // Return only serializable fields return Response.json({ id: product.id, name: product.name, @@ -57,4 +41,4 @@ export async function PATCH( { status: 500 }, ); } -} +}); diff --git a/frontend/app/api/products/[id]/route.ts b/frontend/app/api/products/[id]/route.ts index 8c20ae84..62bddf6b 100644 --- a/frontend/app/api/products/[id]/route.ts +++ b/frontend/app/api/products/[id]/route.ts @@ -1,17 +1,16 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const { id } = await params; +export const PATCH = withAuth(async (req, session, context) => { + const { id } = await context.params; const body = await req.json(); - const authHeaders = await getAuthHeaders(); const res = await fetch(`${API_BASE}/api/products/${id}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return NextResponse.json(data, { status: res.status }); -} +}); diff --git a/frontend/app/api/products/pending/route.ts b/frontend/app/api/products/pending/route.ts index 3e2de77c..e9557a30 100644 --- a/frontend/app/api/products/pending/route.ts +++ b/frontend/app/api/products/pending/route.ts @@ -1,16 +1,15 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function POST(req: NextRequest) { +export const POST = withAuth(async (req, session) => { const body = await req.json(); - const authHeaders = await getAuthHeaders(); const res = await fetch(`${API_BASE}/api/products/pending`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const data = await res.json(); return NextResponse.json(data, { status: res.status }); -} +}); diff --git a/frontend/app/api/products/route.ts b/frontend/app/api/products/route.ts index c0ca7724..d271a64d 100644 --- a/frontend/app/api/products/route.ts +++ b/frontend/app/api/products/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; @@ -15,17 +15,13 @@ export async function GET(req: NextRequest) { return NextResponse.json(data, { status: res.status }); } -export async function POST(req: NextRequest) { +export const POST = withAuth(async (req, session) => { const body = await req.json(); - const authHeaders = await getAuthHeaders(); - // Debug: log auth headers - // eslint-disable-next-line no-console - console.log('API /api/products POST: authHeaders =', authHeaders); const res = await fetch(`${API_BASE}/api/products`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const data = await res.json(); return NextResponse.json(data, { status: res.status }); -} +}); diff --git a/frontend/app/api/profile/route.ts b/frontend/app/api/profile/route.ts index aecdca98..b5813bb2 100644 --- a/frontend/app/api/profile/route.ts +++ b/frontend/app/api/profile/route.ts @@ -1,32 +1,24 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET() { - const authHeaders = await getAuthHeaders(); +export const GET = withAuth(async (_req, session) => { const res = await fetch(`${API_BASE}/api/users/me`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function PATCH(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const PATCH = withAuth(async (request: NextRequest, session) => { const body = await request.json(); const res = await fetch(`${API_BASE}/api/users/me`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/quick-import-proxy/route.ts b/frontend/app/api/quick-import-proxy/route.ts index 3df6bc52..6b4f8aa5 100644 --- a/frontend/app/api/quick-import-proxy/route.ts +++ b/frontend/app/api/quick-import-proxy/route.ts @@ -1,35 +1,30 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; -export async function POST(request: NextRequest) { +export const POST = withAuth(async (request, session) => { try { const contentType = request.headers.get('content-type') ?? ''; const isMultipart = contentType.includes('multipart/form-data'); const backendUrl = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; - const authHeaders = await getAuthHeaders(); const response = await fetch(`${backendUrl}/api/quick-import`, { method: 'POST', body: isMultipart ? await request.formData() : JSON.stringify(await request.json()), - headers: isMultipart ? { ...authHeaders } : { 'Content-Type': 'application/json', ...authHeaders }, + headers: isMultipart + ? { Authorization: `Bearer ${session.accessToken}` } + : { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await response.text(); - return new NextResponse(text, { status: response.status, - headers: { - 'Content-Type': response.headers.get('content-type') ?? 'application/json', - }, + headers: { 'Content-Type': response.headers.get('content-type') ?? 'application/json' }, }); } catch (error) { console.error('[QuickImportProxy] EXCEPTION:', error); - return NextResponse.json( - { message: 'Kunde inte nå importtjänsten.' }, - { status: 503 }, - ); + return NextResponse.json({ message: 'Kunde inte nå importtjänsten.' }, { status: 503 }); } -} +}); diff --git a/frontend/app/api/receipt-alias-proxy/route.ts b/frontend/app/api/receipt-alias-proxy/route.ts index 97132343..8bdde01f 100644 --- a/frontend/app/api/receipt-alias-proxy/route.ts +++ b/frontend/app/api/receipt-alias-proxy/route.ts @@ -1,43 +1,34 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET() { - const authHeaders = await getAuthHeaders(); +export const GET = withAuth(async (_request, session) => { const res = await fetch(`${API_BASE}/api/receipt-aliases`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.json(); const res = await fetch(`${API_BASE}/api/receipt-aliases`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function DELETE(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const id = request.nextUrl.searchParams.get('id'); +export const DELETE = withAuth(async (request, session) => { + const id = new URL(request.url).searchParams.get('id'); const res = await fetch(`${API_BASE}/api/receipt-aliases/${id}`, { method: 'DELETE', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, }); return new NextResponse(null, { status: res.status }); -} +}); diff --git a/frontend/app/api/receipt-import-proxy/route.ts b/frontend/app/api/receipt-import-proxy/route.ts index d1ae91a6..ef716eb0 100644 --- a/frontend/app/api/receipt-import-proxy/route.ts +++ b/frontend/app/api/receipt-import-proxy/route.ts @@ -1,22 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const formData = await request.formData(); const res = await fetch(`${API_BASE}/api/receipt-import`, { method: 'POST', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, body: formData, }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/recipe-preview-proxy/route.ts b/frontend/app/api/recipe-preview-proxy/route.ts index efccff27..3ffbaea1 100644 --- a/frontend/app/api/recipe-preview-proxy/route.ts +++ b/frontend/app/api/recipe-preview-proxy/route.ts @@ -1,31 +1,21 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET(request: NextRequest) { - const authHeaders = await getAuthHeaders(); - const id = request.nextUrl.searchParams.get('id'); +export const GET = withAuth(async (request, session) => { + const id = new URL(request.url).searchParams.get('id'); if (!id) { - return NextResponse.json( - { error: 'Missing id parameter' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Missing id parameter' }, { status: 400 }); } const res = await fetch(`${API_BASE}/api/recipes/${id}/inventory-preview`, { - method: 'GET', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - - return new NextResponse(text, { - status: res.status, - headers: { - 'Content-Type': 'application/json', - }, - }); + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); } \ No newline at end of file diff --git a/frontend/app/api/recipes/[id]/image/route.ts b/frontend/app/api/recipes/[id]/image/route.ts index f7ce3108..7e19725d 100644 --- a/frontend/app/api/recipes/[id]/image/route.ts +++ b/frontend/app/api/recipes/[id]/image/route.ts @@ -1,27 +1,16 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session, context) => { + const { id } = await context.params; const body = await request.text(); - const res = await fetch(`${API_BASE}/api/recipes/${id}/image`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body, - cache: 'no-store', }); - const text = await res.text(); - - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/app/api/recipes/[id]/route.ts b/frontend/app/api/recipes/[id]/route.ts index 69127c6b..2d3927e9 100644 --- a/frontend/app/api/recipes/[id]/route.ts +++ b/frontend/app/api/recipes/[id]/route.ts @@ -1,55 +1,35 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const authHeaders = await getAuthHeaders(); +export const GET = withAuth(async (request, session, context) => { + const { id } = await context.params; const res = await fetch(`${API_BASE}/api/recipes/${id}`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function PATCH( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const authHeaders = await getAuthHeaders(); +export const PATCH = withAuth(async (request, session, context) => { + const { id } = await context.params; const body = await request.json(); const res = await fetch(`${API_BASE}/api/recipes/${id}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), - cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function DELETE( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const authHeaders = await getAuthHeaders(); +export const DELETE = withAuth(async (_request, session, context) => { + const { id } = await context.params; const res = await fetch(`${API_BASE}/api/recipes/${id}`, { method: 'DELETE', - headers: { ...authHeaders }, - cache: 'no-store', + headers: { Authorization: `Bearer ${session.accessToken}` }, }); return new NextResponse(null, { status: res.status }); -} +}); diff --git a/frontend/app/api/recipes/route.ts b/frontend/app/api/recipes/route.ts index 92eaafde..d8ed95fc 100644 --- a/frontend/app/api/recipes/route.ts +++ b/frontend/app/api/recipes/route.ts @@ -1,31 +1,27 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET() { - const authHeaders = await getAuthHeaders(); +export const GET = withAuth(async (_request, session) => { const res = await fetch(`${API_BASE}/api/recipes`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const data = await res.json(); return NextResponse.json(data, { status: res.status }); -} +}); -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.json(); const res = await fetch(`${API_BASE}/api/recipes`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), - cache: 'no-store', }); - const text = await res.text(); return new NextResponse(text, { status: res.status, headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' }, }); -} +}); diff --git a/frontend/app/api/user-products/[productId]/route.ts b/frontend/app/api/user-products/[productId]/route.ts index 7597c7ae..b1c04c5e 100644 --- a/frontend/app/api/user-products/[productId]/route.ts +++ b/frontend/app/api/user-products/[productId]/route.ts @@ -1,17 +1,13 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function DELETE( - _request: NextRequest, - { params }: { params: Promise<{ productId: string }> }, -) { - const { productId } = await params; - const authHeaders = await getAuthHeaders(); +export const DELETE = withAuth(async (_request, session, context) => { + const { productId } = await context.params; const res = await fetch(`${API_BASE}/api/user-products/${productId}`, { method: 'DELETE', - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, }); return new NextResponse(null, { status: res.status }); -} +}); diff --git a/frontend/app/api/user-products/route.ts b/frontend/app/api/user-products/route.ts index 3023fb75..b4c15982 100644 --- a/frontend/app/api/user-products/route.ts +++ b/frontend/app/api/user-products/route.ts @@ -1,32 +1,24 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthHeaders } from '../../../lib/auth-headers'; +import { NextResponse } from 'next/server'; +import { withAuth } from '../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; -export async function GET() { - const authHeaders = await getAuthHeaders(); +export const GET = withAuth(async (_request, session) => { const res = await fetch(`${API_BASE}/api/user-products`, { - headers: { ...authHeaders }, + headers: { Authorization: `Bearer ${session.accessToken}` }, cache: 'no-store', }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); -export async function POST(request: NextRequest) { - const authHeaders = await getAuthHeaders(); +export const POST = withAuth(async (request, session) => { const body = await request.json(); const res = await fetch(`${API_BASE}/api/user-products`, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` }, body: JSON.stringify(body), }); const text = await res.text(); - return new NextResponse(text, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); -} + return new NextResponse(text, { status: res.status, headers: { 'Content-Type': 'application/json' } }); +}); diff --git a/frontend/lib/with-auth.ts b/frontend/lib/with-auth.ts new file mode 100644 index 00000000..4044c405 --- /dev/null +++ b/frontend/lib/with-auth.ts @@ -0,0 +1,35 @@ +/** + * Hjälpfunktion för att wrappa Next.js Route Handlers med NextAuth auth(). + * Löser problemet med att auth() standalone inte fungerar i route handlers + * med Next.js 15+/16 (async cookies-kompatibilitet i NextAuth beta). + * + * request.auth = session-objektet (inkl. accessToken) + */ +import { NextResponse } from 'next/server'; +import { auth } from '../auth'; + +export type AuthedRequest = Request & { auth: { accessToken?: string; user?: any } | null }; + +/** + * Returnerar Authorization-headern från en autentiserad request. + * Kastar 401-svar om sessionen saknar accessToken. + */ +export function getBearer(session: AuthedRequest['auth']): string | null { + if (!session?.accessToken) return null; + return `Bearer ${session.accessToken}`; +} + +/** + * Wrapper: export const GET = withAuth(async (req, session, context) => { ... }) + */ +export function withAuth( + handler: (req: Request, session: NonNullable, context: any) => Promise, +) { + return auth(async function (request: any, context: any) { + const session = request.auth; + if (!session?.accessToken) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + return handler(request, session, context); + }); +}