feat: add TypeScript definitions for next-auth session with accessToken and user details
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 20:09:21 +02:00
parent afd2607000
commit ffe50e5151
135 changed files with 5 additions and 38 deletions
@@ -0,0 +1,30 @@
import { withAuth } from '../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const POST = withAuth(async (req, session) => {
try {
const body = await req.json().catch(() => ({}));
const { productIds } = body;
const res = await fetch(`${API_BASE}/api/products/ai-categorize-bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` },
body: JSON.stringify({ productIds }),
});
if (!res.ok) {
const text = await res.text();
console.error('[api/admin/bulk-categorize] failed:', res.status, text);
return Response.json({ error: `Bulk-AI-kategorisering misslyckades: ${text}` }, { status: res.status });
}
return Response.json(await res.json());
} catch (err) {
console.error('[api/admin/bulk-categorize] error:', err);
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST(req: Request) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const { ids, categoryId } = body as { ids: number[]; categoryId: number | null };
const res = await fetch(`${API_BASE}/api/products/bulk-update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ ids, categoryId }),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Bulk-uppdatering misslyckades' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,37 @@
import { withAuth } from '../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const POST = withAuth(async (req, session) => {
try {
const body = await req.json();
const { name } = body;
if (!name || typeof name !== 'string') {
return Response.json({ error: 'Name is required' }, { status: 400 });
}
const res = await fetch(`${API_BASE}/api/products`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` },
body: JSON.stringify({ name }),
});
if (!res.ok) {
const e = await res.json().catch(() => ({}));
return Response.json({ error: e.message ?? `HTTP ${res.status}` }, { status: res.status });
}
const product = await res.json();
return Response.json({
id: product.id,
name: product.name,
canonicalName: product.canonicalName ?? null,
});
} catch (err) {
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});
@@ -0,0 +1,29 @@
import { withAuth } from '../../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const POST = withAuth(async (_req, session, context) => {
const { id } = await context.params;
const productId = Number(id);
if (!productId) return Response.json({ error: 'Ogiltigt id' }, { status: 400 });
const res = await fetch(`${API_BASE}/api/products/${productId}/restore`, {
method: 'POST',
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await res.json().catch(() => ({}));
return Response.json(data, { status: res.status });
});
export const DELETE = withAuth(async (_req, session, context) => {
const { id } = await context.params;
const productId = Number(id);
if (!productId) return Response.json({ error: 'Ogiltigt id' }, { status: 400 });
const res = await fetch(`${API_BASE}/api/products/${productId}/permanent`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await res.json().catch(() => ({}));
return Response.json(data, { status: res.status });
});
@@ -0,0 +1,11 @@
import { withAuth } from '../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const GET = withAuth(async (_req, session) => {
const res = await fetch(`${API_BASE}/api/products/deleted`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await res.json().catch(() => ([]));
return Response.json(data, { status: res.status });
});
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const body = await req.json();
const res = await fetch(`${API_BASE}/api/inventory/${id}/consume`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte förbruka inventory-rad' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const body = await req.json();
const res = await fetch(`${API_BASE}/api/inventory/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte uppdatera inventory-rad' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const res = await fetch(`${API_BASE}/api/inventory/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte ta bort inventory-rad' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST(req: Request) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const res = await fetch(`${API_BASE}/api/inventory`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte skapa inventory-rad' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,22 @@
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 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}`,
{
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' } });
});
@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST(req: Request) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const { sourceProductId, targetProductId } = body as {
sourceProductId: number;
targetProductId: number;
};
const res = await fetch(`${API_BASE}/api/products/merge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ sourceProductId, targetProductId }),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Sammanslagning misslyckades' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const res = await fetch(`${API_BASE}/api/pantry/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte ta bort baslager-vara' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST(req: Request) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const { productId } = body as { productId: number };
const res = await fetch(`${API_BASE}/api/pantry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ productId }),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte lägga till baslager-vara' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const body = await req.json();
const { status } = body as { status: 'active' | 'rejected' };
const res = await fetch(`${API_BASE}/api/products/${id}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ status }),
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Kunde inte uppdatera status' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,95 @@
import { withAuth } from '../../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const PATCH = withAuth(async (req, session, context) => {
try {
const { id } = await context.params;
const productId = Number(id);
if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 });
const body = await req.json();
const { name, canonicalName, category, subcategory, brand, categoryId, tags } = body;
if (!name || typeof name !== 'string' || !name.trim()) {
return Response.json({ error: 'Namn får inte vara tomt.' }, { status: 400 });
}
const authHeader = `Bearer ${session.accessToken}`;
const patchRes = await fetch(`${API_BASE}/api/products/${productId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({
name: name.trim(),
canonicalName: canonicalName?.trim() || undefined,
category: category?.trim() || null,
subcategory: subcategory?.trim() || null,
brand: brand?.trim() || null,
categoryId: categoryId ?? null,
}),
});
if (!patchRes.ok) {
const text = await patchRes.text();
console.error('[api/admin/product] PATCH failed:', patchRes.status, text);
return Response.json({ error: `Kunde inte uppdatera produkt: ${text}` }, { status: patchRes.status });
}
const tagsRes = await fetch(`${API_BASE}/api/products/${productId}/tags`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({ tags: tags ?? [] }),
});
if (!tagsRes.ok) {
const text = await tagsRes.text();
console.error('[api/admin/product] tags PUT failed:', tagsRes.status, text);
return Response.json({ error: `Kunde inte uppdatera taggar: ${text}` }, { status: tagsRes.status });
}
const fullRes = await fetch(`${API_BASE}/api/products/${productId}`, {
headers: { Authorization: authHeader },
});
if (!fullRes.ok) {
return Response.json({ error: 'Produkt uppdaterad men kunde inte hämtas' }, { status: 500 });
}
return Response.json(await fullRes.json());
} catch (err) {
console.error('[api/admin/product] PATCH error:', err);
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});
export const DELETE = withAuth(async (_req, session, context) => {
try {
const { id } = await context.params;
const productId = Number(id);
if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 });
const res = await fetch(`${API_BASE}/api/products/${productId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${session.accessToken}` },
});
if (!res.ok) {
const text = await res.text();
console.error('[api/admin/product] DELETE failed:', res.status, text);
return Response.json({ error: `Kunde inte ta bort produkt: ${text}` }, { status: res.status });
}
return new Response(null, { status: 204 });
} catch (err) {
console.error('[api/admin/product] DELETE error:', err);
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server';
import { auth } from '../../../../auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export async function POST() {
const session = await auth();
if (!session?.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const res = await fetch(`${API_BASE}/api/products/reset-all`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.accessToken}`,
},
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || 'Återställning misslyckades' }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,29 @@
import { withAuth } from '../../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const GET = withAuth(async (_req, session, context) => {
try {
const { id } = await context.params;
const productId = Number(id);
if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 });
const res = await fetch(`${API_BASE}/api/products/${productId}/suggest-category`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
if (!res.ok) {
const text = await res.text();
console.error('[api/admin/suggest-category] failed:', res.status, text);
return Response.json({ error: `AI-kategorisering misslyckades: ${text}` }, { status: res.status });
}
return Response.json(await res.json());
} catch (err) {
console.error('[api/admin/suggest-category] error:', err);
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});
@@ -0,0 +1,36 @@
import { withAuth } from '../../../../../lib/with-auth';
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
export const PATCH = withAuth(async (req, session, context) => {
try {
const { id } = await context.params;
const productId = parseInt(id, 10);
const body = await req.json();
const { categoryId } = body;
const res = await fetch(`${API_BASE}/api/products/${productId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.accessToken}` },
body: JSON.stringify({ categoryId }),
});
if (!res.ok) {
const e = await res.json().catch(() => ({}));
return Response.json({ error: e.message ?? `HTTP ${res.status}` }, { status: res.status });
}
const product = await res.json();
return Response.json({
id: product.id,
name: product.name,
canonicalName: product.canonicalName ?? null,
categoryId: product.categoryId ?? null,
});
} catch (err) {
return Response.json(
{ error: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 },
);
}
});