ed34b186f9
- New /refer page with share/copy/stats - Fixed modal z-index conflicts (z-50 → z-[60]) across forum, groups, souq, halal-monitor - Seed route: forum categories, marketplace listings, private groups - Mufti/Daie personas now available in Premium tier - Fixed referral share link: falahos.my/mobile/auth?ref=CODE - Fixed client-side persona access check for premium
145 lines
4.0 KiB
TypeScript
145 lines
4.0 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { cookies } from "next/headers";
|
|
import {
|
|
type Provider,
|
|
exchangeCode,
|
|
findOrCreateOAuthUser,
|
|
verifyState,
|
|
} from "@/lib/oauth";
|
|
import { signJWT } from "@/lib/auth";
|
|
|
|
const VALID_PROVIDERS = ["google", "github"];
|
|
|
|
async function handleCallback(
|
|
req: NextRequest,
|
|
provider: string
|
|
): Promise<NextResponse> {
|
|
try {
|
|
// Grab code and state from query params or POST body
|
|
const url = new URL(req.url);
|
|
let code = url.searchParams.get("code");
|
|
let stateParam = url.searchParams.get("state");
|
|
|
|
// Try reading POST body if code wasn't in query params
|
|
if (!code) {
|
|
try {
|
|
const contentType = req.headers.get("content-type") || "";
|
|
if (contentType.includes("form")) {
|
|
const formData = await req.formData();
|
|
code = formData.get("code") as string | null;
|
|
stateParam = formData.get("state") as string | null;
|
|
}
|
|
} catch {
|
|
// not a form POST, that's fine
|
|
}
|
|
}
|
|
|
|
if (!code) {
|
|
return NextResponse.json(
|
|
{ error: "Missing authorization code" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!stateParam) {
|
|
return NextResponse.json(
|
|
{ error: "Missing state parameter" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Verify state from cookie (CSRF protection)
|
|
const cookieStore = await cookies();
|
|
const storedState = cookieStore.get("oauth_state")?.value;
|
|
|
|
if (!storedState) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Missing state cookie. Please start the OAuth flow again.",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const statePayload = await verifyState(stateParam);
|
|
if (!statePayload) {
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
"Invalid or expired state. Please start the OAuth flow again.",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Verify the state and provider match
|
|
if (statePayload.provider !== provider) {
|
|
return NextResponse.json(
|
|
{ error: "State/provider mismatch" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Clear the state cookie
|
|
cookieStore.set("oauth_state", "", {
|
|
httpOnly: true,
|
|
path: "/",
|
|
maxAge: 0,
|
|
});
|
|
|
|
// Build the redirect URI (must match the one used in the authorization request)
|
|
const forwardedProto = req.headers.get("x-forwarded-proto") || "https";
|
|
const rawHost = req.headers.get("host") || "";
|
|
const isDockerHost = /^[a-f0-9]{12,}$/i.test(rawHost.split(":")[0]);
|
|
const forwardedHost = req.headers.get("x-forwarded-host") || (isDockerHost ? "falahos.my" : rawHost);
|
|
const baseUrl = `${forwardedProto}://${forwardedHost}`;
|
|
const redirectUri = `${baseUrl}/mobile/api/auth/${provider}/callback`;
|
|
|
|
// Exchange code for user info
|
|
const providerUser = await exchangeCode(
|
|
provider as Provider,
|
|
code,
|
|
redirectUri
|
|
);
|
|
|
|
// Find or create user in database
|
|
const user = await findOrCreateOAuthUser(
|
|
provider as Provider,
|
|
providerUser
|
|
);
|
|
|
|
// Generate JWT
|
|
const token = await signJWT({
|
|
userId: user.id,
|
|
email: user.email,
|
|
isPremium: user.isPremium,
|
|
isPro: user.isPro,
|
|
});
|
|
|
|
// Redirect to frontend callback page with token
|
|
const frontendUrl = `${baseUrl}/mobile/auth/callback?token=${encodeURIComponent(token)}`;
|
|
return NextResponse.redirect(frontendUrl);
|
|
} catch (error) {
|
|
console.error(`${provider} OAuth callback error:`, error);
|
|
const message =
|
|
error instanceof Error ? error.message : "OAuth callback failed";
|
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function GET(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ provider: string }> }
|
|
) {
|
|
const { provider } = await params;
|
|
if (!VALID_PROVIDERS.includes(provider)) {
|
|
return NextResponse.json(
|
|
{
|
|
error: `Unsupported provider. Must be one of: ${VALID_PROVIDERS.join(", ")}`,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
return handleCallback(req, provider);
|
|
}
|