120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import { cookies } from 'next/headers';
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
function getSafeRedirectUrl(targetPath: string, request: NextRequest): URL {
|
|
const forwardedHost = request.headers.get('x-forwarded-host')?.split(',')[0].trim()
|
|
?? request.headers.get('host')
|
|
?? 'motorsignal.app';
|
|
const forwardedProto = request.headers.get('x-forwarded-proto')?.split(',')[0].trim()
|
|
?? (process.env.NODE_ENV === 'production' ? 'https' : 'http');
|
|
return new URL(targetPath, `${forwardedProto}://${forwardedHost}`);
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const cookieStore = await cookies();
|
|
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
|
|
|
|
if (!riderToken) {
|
|
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
|
|
}
|
|
|
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
|
|
|
try {
|
|
const upstream = await fetch(`${baseUrl}/api/v1/ops/session/bootstrap`, {
|
|
method: 'POST',
|
|
headers: {
|
|
authorization: `Bearer ${riderToken}`,
|
|
accept: 'application/json',
|
|
},
|
|
cache: 'no-store',
|
|
});
|
|
|
|
if (upstream.ok) {
|
|
const data = (await upstream.json()) as { ops_token?: string };
|
|
const opsToken = data.ops_token;
|
|
|
|
if (opsToken) {
|
|
const redirectUrl = getSafeRedirectUrl('/admin', request);
|
|
const response = NextResponse.redirect(redirectUrl);
|
|
const opsCookieOptions = {
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'lax' as const,
|
|
path: '/',
|
|
maxAge: 60 * 60 * 8,
|
|
};
|
|
|
|
response.cookies.set('__Host-motorsignal_ops', opsToken, opsCookieOptions);
|
|
response.cookies.set('motorsignal_ops', opsToken, {
|
|
...opsCookieOptions,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
});
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
if (upstream.status === 403) {
|
|
return NextResponse.redirect(getSafeRedirectUrl('/admin?error=forbidden', request));
|
|
}
|
|
|
|
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
|
|
} catch {
|
|
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const cookieStore = await cookies();
|
|
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
|
|
|
|
if (!riderToken) {
|
|
return NextResponse.json({ detail: 'Rider authentication required.' }, { status: 401 });
|
|
}
|
|
|
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
|
|
|
try {
|
|
const upstream = await fetch(`${baseUrl}/api/v1/ops/session/bootstrap`, {
|
|
method: 'POST',
|
|
headers: {
|
|
authorization: `Bearer ${riderToken}`,
|
|
accept: 'application/json',
|
|
},
|
|
cache: 'no-store',
|
|
});
|
|
|
|
if (!upstream.ok) {
|
|
const err = await upstream.json().catch(() => ({}));
|
|
return NextResponse.json(err, { status: upstream.status });
|
|
}
|
|
|
|
const data = (await upstream.json()) as { ops_token?: string };
|
|
const opsToken = data.ops_token;
|
|
|
|
if (!opsToken) {
|
|
return NextResponse.json({ detail: 'Failed to issue operations token.' }, { status: 500 });
|
|
}
|
|
|
|
const response = NextResponse.json({ success: true }, { status: 200 });
|
|
const opsCookieOptions = {
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'lax' as const,
|
|
path: '/',
|
|
maxAge: 60 * 60 * 8,
|
|
};
|
|
|
|
response.cookies.set('__Host-motorsignal_ops', opsToken, opsCookieOptions);
|
|
response.cookies.set('motorsignal_ops', opsToken, {
|
|
...opsCookieOptions,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
});
|
|
|
|
return response;
|
|
} catch {
|
|
return NextResponse.json({ detail: 'Operations bootstrap unavailable.' }, { status: 503 });
|
|
}
|
|
}
|