diff --git a/.idea/db-forest-config.xml b/.idea/db-forest-config.xml new file mode 100644 index 0000000..454b8f6 --- /dev/null +++ b/.idea/db-forest-config.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/admin/platforms/page.tsx b/app/admin/platforms/page.tsx index 151f747..fc52b37 100644 --- a/app/admin/platforms/page.tsx +++ b/app/admin/platforms/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from "react"; import { Plus, Pencil, Trash2, X } from "lucide-react"; -import { useAuth } from "@/context/AuthContext"; import { createPlatform, deletePlatform, @@ -15,7 +14,6 @@ import { } from "@/lib/api/platforms"; export default function AdminPlatformsPage() { - const { getAuthHeaders } = useAuth(); const [platforms, setPlatforms] = useState([]); const [loading, setLoading] = useState(true); @@ -36,7 +34,7 @@ export default function AdminPlatformsPage() { try { setLoading(true); setError(null); - const data = await fetchPlatforms(getAuthHeaders()); + const data = await fetchPlatforms({}); setPlatforms(data); } catch (e: any) { console.error(e); @@ -77,7 +75,7 @@ export default function AdminPlatformsPage() { try { setSaving(true); setError(null); - await deletePlatform(getAuthHeaders(), p.id); + await deletePlatform({}, p.id); await load(); } catch (e: any) { console.error(e); @@ -103,7 +101,7 @@ export default function AdminPlatformsPage() { setError(null); if (modalMode === "create") { - await createPlatform(getAuthHeaders(), { ...form, key, label }); + await createPlatform({}, { ...form, key, label }); } else if (selected) { const update: UpdatePlatformDto = {}; if (key !== selected.key) update.key = key; @@ -111,7 +109,7 @@ export default function AdminPlatformsPage() { if ((form.isActive ?? true) !== selected.isActive) update.isActive = form.isActive; - await updatePlatform(getAuthHeaders(), selected.id, update); + await updatePlatform({}, selected.id, update); } setShowModal(false); diff --git a/app/admin/products/_lib/api.ts b/app/admin/products/_lib/api.ts index 5e98c10..6230c41 100644 --- a/app/admin/products/_lib/api.ts +++ b/app/admin/products/_lib/api.ts @@ -1,4 +1,4 @@ -import { API_BASE, getToken, qs } from "./auth"; +import { API_BASE, qs } from "./auth"; import type { AdminProductRow, PageResponse, @@ -9,25 +9,14 @@ import type { import type { CaliberDto } from "./types"; - -async function authedFetch(url: string, init?: RequestInit) { - const token = getToken(); - const res = await fetch(url, { - credentials: "include", - cache: "no-store", - ...(init ?? {}), - headers: { - Accept: "application/json", - ...(init?.headers ?? {}), - ...(token ? { Authorization: `Bearer ${token}` } : {}), - } as any, - }); - return res; -} - export async function fetchAdminRoles(params: Record) { - const res = await authedFetch( - `${API_BASE}/api/v1/admin/products/roles?${qs(params)}` + const queryString = qs(params); + const res = await fetch( + `/api/admin/products/roles${queryString ? `?${queryString}` : ''}`, + { + credentials: "include", + cache: "no-store", + } ); if (!res.ok) throw new Error(`Failed to load roles (${res.status}): ${await res.text()}`); return (await res.json()) as string[]; @@ -35,8 +24,13 @@ export async function fetchAdminRoles(params: Record) { // Facet/filtering can still use distinct values seen on products (optional) export async function fetchProductCaliberFacet(params: Record) { - const res = await authedFetch( - `${API_BASE}/api/v1/admin/products/calibers?${qs(params)}` + const queryString = qs(params); + const res = await fetch( + `/api/admin/products/calibers${queryString ? `?${queryString}` : ''}`, + { + credentials: "include", + cache: "no-store", + } ); if (!res.ok) throw new Error( @@ -46,8 +40,13 @@ export async function fetchProductCaliberFacet(params: Record) { } export async function fetchAdminProducts(params: Record) { - const res = await authedFetch( - `${API_BASE}/api/v1/admin/products?${qs(params)}` + const queryString = qs(params); + const res = await fetch( + `/api/admin/products${queryString ? `?${queryString}` : ''}`, + { + credentials: "include", + cache: "no-store", + } ); if (!res.ok) throw new Error(`Failed to load admin products (${res.status}): ${await res.text()}`); return (await res.json()) as PageResponse; @@ -84,14 +83,11 @@ export async function bulkUpdate( classificationReason: "Admin bulk override"; }> ) { - const token = getToken(); - - const res = await fetch(`${API_BASE}/api/v1/admin/products/bulk`, { + const res = await fetch(`/api/admin/products/bulk`, { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ productIds, ...set }), }); @@ -103,8 +99,13 @@ export async function bulkUpdate( export async function fetchAdminCalibers(params: Record) { - const res = await authedFetch( - `${API_BASE}/api/v1/admin/calibers?${qs(params)}` + const queryString = qs(params); + const res = await fetch( + `/api/admin/calibers${queryString ? `?${queryString}` : ''}`, + { + credentials: "include", + cache: "no-store", + } ); if (!res.ok) { throw new Error(`Failed to load calibers (${res.status}): ${await res.text()}`); diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 36404c1..fb9941e 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -21,7 +21,7 @@ export default function AdminUsersPage() { setLoading(true); setError(null); - const res = await fetch(`${API_BASE_URL}/admin/users`, { + const res = await fetch(`/api/admin/users`, { headers: { ...getAuthHeaders(), }, diff --git a/app/api/admin/calibers/route.ts b/app/api/admin/calibers/route.ts new file mode 100644 index 0000000..9b8ef1f --- /dev/null +++ b/app/api/admin/calibers/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function GET(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/calibers${queryString ? `?${queryString}` : ''}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/enrichment/[id]/[action]/route.ts b/app/api/admin/enrichment/[id]/[action]/route.ts new file mode 100644 index 0000000..d1578cc --- /dev/null +++ b/app/api/admin/enrichment/[id]/[action]/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string; action: string } } +) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id, action } = params; + + const res = await fetch( + `${API_BASE_URL}/api/admin/enrichment/${id}/${action}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + const contentType = res.headers.get('content-type'); + if (contentType?.includes('application/json')) { + return NextResponse.json(await res.json()); + } + + return NextResponse.json({ success: true }); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/enrichment/ai/run/route.ts b/app/api/admin/enrichment/ai/run/route.ts new file mode 100644 index 0000000..3241f75 --- /dev/null +++ b/app/api/admin/enrichment/ai/run/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function POST(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/admin/enrichment/ai/run${queryString ? `?${queryString}` : ''}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + const contentType = res.headers.get('content-type'); + if (contentType?.includes('application/json')) { + return NextResponse.json(await res.json()); + } + + return NextResponse.json({ success: true }); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/enrichment/queue2/route.ts b/app/api/admin/enrichment/queue2/route.ts new file mode 100644 index 0000000..e033a6a --- /dev/null +++ b/app/api/admin/enrichment/queue2/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { fixImageUrls } from '@/lib/server/image-utils'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function GET(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/admin/enrichment/queue2${queryString ? `?${queryString}` : ''}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + const data = await res.json(); + + // Fix mixed content warnings: upgrade HTTP image URLs to HTTPS + return NextResponse.json(fixImageUrls(data)); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/enrichment/run/route.ts b/app/api/admin/enrichment/run/route.ts new file mode 100644 index 0000000..c2c61f6 --- /dev/null +++ b/app/api/admin/enrichment/run/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function POST(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/admin/enrichment/run${queryString ? `?${queryString}` : ''}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + const contentType = res.headers.get('content-type'); + if (contentType?.includes('application/json')) { + return NextResponse.json(await res.json()); + } + + return NextResponse.json({ success: true }); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/platforms/[id]/route.ts b/app/api/admin/platforms/[id]/route.ts new file mode 100644 index 0000000..199abf3 --- /dev/null +++ b/app/api/admin/platforms/[id]/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +async function getAuthHeader() { + const token = cookies().get('session_token')?.value; + if (!token) throw new Error('Unauthorized'); + return { Authorization: `Bearer ${token}` }; +} + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const headers = await getAuthHeader(); + const body = await request.json(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/platforms/${params.id}`, + { + method: 'PATCH', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const headers = await getAuthHeader(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/platforms/${params.id}`, + { method: 'DELETE', headers } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json({ ok: true }); + } catch (err: any) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } +} diff --git a/app/api/admin/platforms/route.ts b/app/api/admin/platforms/route.ts new file mode 100644 index 0000000..91ecce3 --- /dev/null +++ b/app/api/admin/platforms/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +async function getAuthHeader() { + const token = cookies().get('session_token')?.value; + if (!token) throw new Error('Unauthorized'); + return { Authorization: `Bearer ${token}` }; +} + +export async function GET(request: NextRequest) { + try { + const headers = await getAuthHeader(); + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/platforms${queryString ? `?${queryString}` : ''}`, + { + headers: { + ...headers, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Unauthorized' }, + { status: 401 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const headers = await getAuthHeader(); + const body = await request.json(); + + const res = await fetch(`${API_BASE_URL}/api/v1/admin/platforms`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Unauthorized' }, + { status: 401 } + ); + } +} diff --git a/app/api/admin/products/bulk/route.ts b/app/api/admin/products/bulk/route.ts new file mode 100644 index 0000000..26ae31f --- /dev/null +++ b/app/api/admin/products/bulk/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function PATCH(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/products/bulk`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/products/calibers/route.ts b/app/api/admin/products/calibers/route.ts new file mode 100644 index 0000000..a5fe1f5 --- /dev/null +++ b/app/api/admin/products/calibers/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function GET(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/products/calibers${queryString ? `?${queryString}` : ''}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/products/roles/route.ts b/app/api/admin/products/roles/route.ts new file mode 100644 index 0000000..37e01f9 --- /dev/null +++ b/app/api/admin/products/roles/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function GET(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/products/roles${queryString ? `?${queryString}` : ''}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/products/route.ts b/app/api/admin/products/route.ts new file mode 100644 index 0000000..c694270 --- /dev/null +++ b/app/api/admin/products/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { fixImageUrls } from '@/lib/server/image-utils'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function GET(request: NextRequest) { + try { + const token = cookies().get('session_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/products${queryString ? `?${queryString}` : ''}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + const data = await res.json(); + return NextResponse.json(fixImageUrls(data)); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/users/[id]/route.ts b/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..199abf3 --- /dev/null +++ b/app/api/admin/users/[id]/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +async function getAuthHeader() { + const token = cookies().get('session_token')?.value; + if (!token) throw new Error('Unauthorized'); + return { Authorization: `Bearer ${token}` }; +} + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const headers = await getAuthHeader(); + const body = await request.json(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/platforms/${params.id}`, + { + method: 'PATCH', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const headers = await getAuthHeader(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/platforms/${params.id}`, + { method: 'DELETE', headers } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json({ ok: true }); + } catch (err: any) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } +} diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts new file mode 100644 index 0000000..b3a1807 --- /dev/null +++ b/app/api/admin/users/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +async function getAuthHeader() { + const token = cookies().get('session_token')?.value; + if (!token) throw new Error('Unauthorized'); + return { Authorization: `Bearer ${token}` }; +} + +export async function GET(request: NextRequest) { + try { + const headers = await getAuthHeader(); + const searchParams = request.nextUrl.searchParams; + const queryString = searchParams.toString(); + + const res = await fetch( + `${API_BASE_URL}/api/v1/admin/users${queryString ? `?${queryString}` : ''}`, + { + headers: { + ...headers, + Accept: 'application/json', + }, + } + ); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + const data = await res.json(); + return NextResponse.json(data); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Unauthorized' }, + { status: 401 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const headers = await getAuthHeader(); + const body = await request.json(); + + const res = await fetch(`${API_BASE_URL}/api/v1/admin/users`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Unauthorized' }, + { status: 401 } + ); + } +} diff --git a/app/api/auth/beta/signup/route.ts b/app/api/auth/beta/signup/route.ts new file mode 100644 index 0000000..e517ec4 --- /dev/null +++ b/app/api/auth/beta/signup/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const res = await fetch(`${API_BASE_URL}/api/auth/beta/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/auth/magic/exchange/route.ts b/app/api/auth/magic/exchange/route.ts new file mode 100644 index 0000000..93974d8 --- /dev/null +++ b/app/api/auth/magic/exchange/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const res = await fetch(`${API_BASE_URL}/api/auth/magic/exchange`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return NextResponse.json( + { error: await res.text() }, + { status: res.status } + ); + } + + return NextResponse.json(await res.json()); + } catch (err: any) { + return NextResponse.json( + { error: err?.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/lib/api/platforms.ts b/lib/api/platforms.ts index 5ba935a..952680b 100644 --- a/lib/api/platforms.ts +++ b/lib/api/platforms.ts @@ -14,9 +14,6 @@ export type CreatePlatformDto = { export type UpdatePlatformDto = Partial; -const API_BASE = - process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; - function normalizePlatform(raw: any): Platform { const id = Number(raw?.id); const key = String(raw?.key ?? "").trim(); @@ -33,9 +30,9 @@ function normalizePlatform(raw: any): Platform { return { id, key, label, isActive }; } -export async function fetchPlatforms(tokenHeaders: HeadersInit): Promise { - const res = await fetch(`${API_BASE}/api/platforms`, { - headers: { ...tokenHeaders }, +export async function fetchPlatforms(_tokenHeaders: HeadersInit): Promise { + const res = await fetch('/api/admin/platforms', { + credentials: 'include', }); if (!res.ok) { @@ -48,15 +45,15 @@ export async function fetchPlatforms(tokenHeaders: HeadersInit): Promise { - const res = await fetch(`${API_BASE}/api/platforms`, { + const res = await fetch('/api/admin/platforms', { method: "POST", headers: { "Content-Type": "application/json", - ...tokenHeaders, }, + credentials: 'include', body: JSON.stringify(dto), }); @@ -69,17 +66,17 @@ export async function createPlatform( } export async function updatePlatform( - tokenHeaders: HeadersInit, + _tokenHeaders: HeadersInit, id: number, dto: UpdatePlatformDto ): Promise { - const res = await fetch(`${API_BASE}/api/platforms/${id}`, { + const res = await fetch(`/api/admin/platforms/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json", - ...tokenHeaders, }, - body: JSON.stringify({ isActive: dto.isActive }), + credentials: 'include', + body: JSON.stringify(dto), }); if (!res.ok) { @@ -90,10 +87,10 @@ export async function updatePlatform( return normalizePlatform(await res.json()); } -export async function deletePlatform(tokenHeaders: HeadersInit, id: number): Promise { - const res = await fetch(`${API_BASE}/api/platforms/${id}`, { +export async function deletePlatform(_tokenHeaders: HeadersInit, id: number): Promise { + const res = await fetch(`/api/admin/platforms/${id}`, { method: "DELETE", - headers: { ...tokenHeaders }, + credentials: 'include', }); if (!res.ok) { diff --git a/lib/server/image-utils.ts b/lib/server/image-utils.ts new file mode 100644 index 0000000..eae2202 --- /dev/null +++ b/lib/server/image-utils.ts @@ -0,0 +1,43 @@ +/** + * Fix mixed content warnings by upgrading HTTP image URLs to HTTPS + * This handles image URLs stored in the database as http:// which cause + * browser warnings when loaded from HTTPS pages. + */ +export function fixImageUrl(url: string | null | undefined): string | null | undefined { + if (!url || typeof url !== 'string') return url; + return url.replace(/^http:\/\//i, 'https://'); +} + +/** + * Recursively fix all image URL fields in an object + * Common fields: imageUrl, mainImageUrl, coverImageUrl, thumbnailUrl, etc. + */ +export function fixImageUrls(data: any): any { + if (!data) return data; + + if (Array.isArray(data)) { + return data.map(item => fixImageUrls(item)); + } + + if (typeof data === 'object') { + const fixed: any = {}; + for (const [key, value] of Object.entries(data)) { + // Fix any field that looks like an image URL + if ( + (key.toLowerCase().includes('image') || + key.toLowerCase().includes('photo') || + key.toLowerCase().includes('picture')) && + typeof value === 'string' + ) { + fixed[key] = fixImageUrl(value); + } else if (typeof value === 'object') { + fixed[key] = fixImageUrls(value); + } else { + fixed[key] = value; + } + } + return fixed; + } + + return data; +} diff --git a/middleware.ts b/middleware.ts index 64fbb12..0af3034 100644 --- a/middleware.ts +++ b/middleware.ts @@ -47,6 +47,8 @@ export function middleware(req: NextRequest) { const { pathname } = req.nextUrl; const token = req.cookies.get("session_token")?.value; const isLoggedIn = Boolean(token); + console.log("is logged in: " + isLoggedIn); + console.log("MW:", req.nextUrl.pathname) // IMPORTANT: Use a server-only env var for middleware behavior. // Set LAUNCH_ONLY_ROOT=true in your Docker compose.