152 lines
3.8 KiB
TypeScript
152 lines
3.8 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
import {
|
|
applyMfaCookie,
|
|
applySessionCookie,
|
|
clearMfaCookie,
|
|
clearSessionCookie,
|
|
deriveMaxAgeFromExpires,
|
|
MFA_COOKIE_NAME,
|
|
} from "@lib/authGateway";
|
|
import { getAccountServiceApiBaseUrl } from "@server/serviceConfig";
|
|
|
|
const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl();
|
|
|
|
type VerifyPayload = {
|
|
token?: string;
|
|
code?: string;
|
|
totp?: string;
|
|
};
|
|
|
|
type AccountVerifyResponse = {
|
|
token?: string;
|
|
expiresAt?: string;
|
|
mfaToken?: string;
|
|
mfaTicket?: string;
|
|
error?: string;
|
|
retryAt?: string;
|
|
user?: Record<string, unknown> | null;
|
|
mfa?: Record<string, unknown> | null;
|
|
};
|
|
|
|
function normalizeString(value: unknown) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function normalizeCode(value: unknown) {
|
|
return typeof value === "string" ? value.replace(/\D/g, "").slice(0, 6) : "";
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const cookieStore = await cookies();
|
|
let payload: VerifyPayload;
|
|
try {
|
|
payload = (await request.json()) as VerifyPayload;
|
|
} catch (error) {
|
|
console.error("Failed to decode MFA verification payload", error);
|
|
return NextResponse.json(
|
|
{ success: false, error: "invalid_request", needMfa: true },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const cookieToken = cookieStore.get(MFA_COOKIE_NAME)?.value ?? "";
|
|
const token = normalizeString(payload?.token || cookieToken);
|
|
const code = normalizeCode(payload?.code ?? payload?.totp);
|
|
|
|
if (!token) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "mfa_token_required", needMfa: true },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
if (!code) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "mfa_code_required", needMfa: true },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${ACCOUNT_API_BASE}/mfa/verify`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ mfaToken: token, code }),
|
|
cache: "no-store",
|
|
});
|
|
|
|
const data = (await response
|
|
.json()
|
|
.catch(() => ({}))) as AccountVerifyResponse;
|
|
|
|
if (
|
|
response.ok &&
|
|
typeof data?.token === "string" &&
|
|
data.token.length > 0
|
|
) {
|
|
const result = NextResponse.json({
|
|
success: true,
|
|
error: null,
|
|
needMfa: false,
|
|
data,
|
|
});
|
|
applySessionCookie(
|
|
result,
|
|
data.token,
|
|
deriveMaxAgeFromExpires(data?.expiresAt),
|
|
request.headers.get("host") ?? undefined,
|
|
);
|
|
clearMfaCookie(result);
|
|
return result;
|
|
}
|
|
|
|
const errorCode =
|
|
typeof data?.error === "string" ? data.error : "mfa_verification_failed";
|
|
const result = NextResponse.json(
|
|
{ success: false, error: errorCode, needMfa: true, data },
|
|
{ status: response.status || 400 },
|
|
);
|
|
|
|
const nextToken =
|
|
typeof data?.mfaToken === "string" && data.mfaToken.trim()
|
|
? data.mfaToken
|
|
: typeof data?.mfaTicket === "string" && data.mfaTicket.trim()
|
|
? data.mfaTicket
|
|
: "";
|
|
|
|
if (nextToken) {
|
|
applyMfaCookie(result, nextToken);
|
|
} else {
|
|
applyMfaCookie(result, token);
|
|
}
|
|
|
|
clearSessionCookie(result, request.headers.get("host") ?? undefined);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Account service MFA verification proxy failed", error);
|
|
const result = NextResponse.json(
|
|
{ success: false, error: "account_service_unreachable", needMfa: true },
|
|
{ status: 502 },
|
|
);
|
|
applyMfaCookie(result, token);
|
|
clearSessionCookie(result, request.headers.get("host") ?? undefined);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
export function GET() {
|
|
return NextResponse.json(
|
|
{ success: false, error: "method_not_allowed", needMfa: true },
|
|
{
|
|
status: 405,
|
|
headers: {
|
|
Allow: "POST",
|
|
},
|
|
},
|
|
);
|
|
}
|