Files
caddy-proxy-manager/app/api/auth/link-account/route.ts
akanealw 99819b70ff
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
added caddy-proxy-manager for testing
2026-04-21 22:49:08 +00:00

108 lines
3.2 KiB
TypeScript
Executable File

import { NextRequest, NextResponse } from "next/server";
import { retrieveLinkingToken, verifyLinkingToken, verifyAndLinkOAuth } from "@/src/lib/services/account-linking";
import { createAuditEvent } from "@/src/lib/models/audit";
import { isRateLimited, registerFailedAttempt, resetAttempts } from "@/src/lib/rate-limit";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { linkingId, password } = body;
if (!linkingId || !password) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}
// Retrieve and consume the linking token server-side — the raw JWT never reaches the browser
const rawToken = await retrieveLinkingToken(linkingId);
if (!rawToken) {
return NextResponse.json(
{ error: "Authentication failed" },
{ status: 401 }
);
}
const tokenPayload = await verifyLinkingToken(rawToken);
if (!tokenPayload) {
return NextResponse.json(
{ error: "Authentication failed" },
{ status: 401 }
);
}
// Rate limiting: check before attempting password verification
const rateLimitKey = `oauth-link-verify:${tokenPayload.userId}`;
const rateLimitCheck = isRateLimited(rateLimitKey);
if (rateLimitCheck.blocked) {
await createAuditEvent({
userId: tokenPayload.userId,
action: "oauth_link_rate_limited",
entityType: "user",
entityId: tokenPayload.userId,
summary: `OAuth linking rate limited: too many password attempts`,
data: JSON.stringify({ provider: tokenPayload.provider })
});
return NextResponse.json(
{ error: "Too many attempts. Please try again later." },
{ status: 429 }
);
}
// Verify password and link OAuth account
const success = await verifyAndLinkOAuth(
tokenPayload.userId,
password,
tokenPayload.provider,
tokenPayload.providerAccountId
);
if (!success) {
// Count this failure against the rate limit
registerFailedAttempt(rateLimitKey);
await createAuditEvent({
userId: tokenPayload.userId,
action: "oauth_link_password_failed",
entityType: "user",
entityId: tokenPayload.userId,
summary: `Failed password verification during OAuth linking`,
data: JSON.stringify({ provider: tokenPayload.provider })
});
return NextResponse.json(
{ error: "Authentication failed" },
{ status: 401 }
);
}
// Success — clear rate limit for this user
resetAttempts(rateLimitKey);
await createAuditEvent({
userId: tokenPayload.userId,
action: "account_linked",
entityType: "user",
entityId: tokenPayload.userId,
summary: `OAuth account manually linked: ${tokenPayload.provider}`,
data: JSON.stringify({
provider: tokenPayload.provider,
email: tokenPayload.email
})
});
return NextResponse.json({
success: true,
message: "Account linked successfully"
});
} catch (error) {
console.error("Account linking error:", error);
return NextResponse.json(
{ error: "Failed to link account" },
{ status: 500 }
);
}
}