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.