feat(admin-web): add experiments + ab-testing proxy routes, fix webhooks deliveries
New proxy routes: - /api/experiments → rewrites to /api/ab-testing/experiments (base + catch-all) - /api/ab-testing/[...path] → /api/ab-testing/* (for suggestions, hypotheses) Bug fix: - B20: webhooks page called GET /webhooks/deliveries (404) — removed broken call, backend only has GET /webhooks/subscriptions/:id/deliveries (TODO Q4)
This commit is contained in:
parent
6d9b687b49
commit
8c45e440df
@ -73,13 +73,13 @@ export default function WebhooksPage() {
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const [sData, dData] = await Promise.all([apiFetch('subscriptions'), apiFetch('deliveries')]);
|
||||
const sData = await apiFetch('subscriptions');
|
||||
setSubs(
|
||||
Array.isArray(sData?.subscriptions) ? sData.subscriptions : Array.isArray(sData) ? sData : []
|
||||
);
|
||||
setDeliveries(
|
||||
Array.isArray(dData?.deliveries) ? dData.deliveries : Array.isArray(dData) ? dData : []
|
||||
);
|
||||
// TODO Q4: Backend has no top-level GET /webhooks/deliveries. Deliveries are per-subscription
|
||||
// via GET /webhooks/subscriptions/:id/deliveries. Wire up per-subscription delivery loading.
|
||||
setDeliveries([]);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* A/B Testing API proxy — forwards requests to platform-service.
|
||||
*
|
||||
* /api/ab-testing/* → platform-service /api/ab-testing/*
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCurrentUserFromRequest } from '@/lib/auth-server';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
|
||||
|
||||
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
try {
|
||||
const caller = await getCurrentUserFromRequest(req);
|
||||
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const { path } = await params;
|
||||
const targetPath = `/api/ab-testing/${path.join('/')}`;
|
||||
const qs = new URL(req.url).search;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
|
||||
'x-user-id': caller.id,
|
||||
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
|
||||
};
|
||||
const fetchOptions: RequestInit = { method: req.method, headers };
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
|
||||
const res = await fetch(`${PLATFORM_URL}${targetPath}${qs}`, fetchOptions);
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
|
||||
} catch (error) {
|
||||
logError('A/B Testing proxy error', error);
|
||||
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Experiments API proxy — forwards requests to platform-service.
|
||||
*
|
||||
* /api/experiments/* → platform-service /api/ab-testing/experiments/*
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCurrentUserFromRequest } from '@/lib/auth-server';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
|
||||
|
||||
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
try {
|
||||
const caller = await getCurrentUserFromRequest(req);
|
||||
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const { path } = await params;
|
||||
const targetPath = `/api/ab-testing/experiments/${path.join('/')}`;
|
||||
const qs = new URL(req.url).search;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
|
||||
'x-user-id': caller.id,
|
||||
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
|
||||
};
|
||||
const fetchOptions: RequestInit = { method: req.method, headers };
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
|
||||
const res = await fetch(`${PLATFORM_URL}${targetPath}${qs}`, fetchOptions);
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
|
||||
} catch (error) {
|
||||
logError('Experiments proxy error', error);
|
||||
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
return proxy(req, ctx);
|
||||
}
|
||||
39
dashboards/admin-web/src/app/api/experiments/route.ts
Normal file
39
dashboards/admin-web/src/app/api/experiments/route.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Experiments base route — handles GET /api/experiments.
|
||||
* Rewrites to platform-service GET /api/ab-testing/experiments.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCurrentUserFromRequest } from '@/lib/auth-server';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
|
||||
|
||||
async function proxyBase(req: NextRequest) {
|
||||
try {
|
||||
const caller = await getCurrentUserFromRequest(req);
|
||||
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const qs = new URL(req.url).search;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
|
||||
'x-user-id': caller.id,
|
||||
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
|
||||
};
|
||||
const fetchOptions: RequestInit = { method: req.method, headers };
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
|
||||
const res = await fetch(`${PLATFORM_URL}/api/ab-testing/experiments${qs}`, fetchOptions);
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
|
||||
} catch (error) {
|
||||
logError('Experiments base proxy error', error);
|
||||
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return proxyBase(req);
|
||||
}
|
||||
export async function POST(req: NextRequest) {
|
||||
return proxyBase(req);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user