fix: use APP_URL for OAuth/verify redirects behind Dokploy proxy

Behind Traefik, request.url resolves to http://0.0.0.0:3000/... (the
internal Docker address). Using that as the redirect base sent browsers
to 0.0.0.0, causing ERR_SSL_PROTOCOL_ERROR. Switch all server-side
redirects in the Google callback and verify-email routes to use
NEXT_PUBLIC_APP_URL (with tia.manohargupta.com fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-24 13:01:01 +05:30
parent 6bdaade777
commit b91595316d
2 changed files with 13 additions and 9 deletions

View file

@ -6,6 +6,8 @@ import { logAudit } from "@/lib/audit";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com";
export async function GET(request: Request) { export async function GET(request: Request) {
const url = new URL(request.url); const url = new URL(request.url);
const code = url.searchParams.get("code"); const code = url.searchParams.get("code");
@ -17,7 +19,7 @@ export async function GET(request: Request) {
// 1. CSRF / integrity check — returned state must match what we sent. // 1. CSRF / integrity check — returned state must match what we sent.
if (!code || !state || !storedState || state !== storedState || !verifier) { if (!code || !state || !storedState || state !== storedState || !verifier) {
return NextResponse.redirect(new URL("/login?error=oauth", request.url)); return NextResponse.redirect(new URL("/login?error=oauth", APP_URL));
} }
try { try {
@ -31,7 +33,7 @@ export async function GET(request: Request) {
{ headers: { Authorization: `Bearer ${tokens.accessToken()}` } }, { headers: { Authorization: `Bearer ${tokens.accessToken()}` } },
); );
if (!profileRes.ok) { if (!profileRes.ok) {
return NextResponse.redirect(new URL("/login?error=oauth", request.url)); return NextResponse.redirect(new URL("/login?error=oauth", APP_URL));
} }
const profile = (await profileRes.json()) as { const profile = (await profileRes.json()) as {
sub: string; email: string; email_verified: boolean; sub: string; email: string; email_verified: boolean;
@ -40,7 +42,7 @@ export async function GET(request: Request) {
// 4. HARD requirement: Google must confirm the email is verified. // 4. HARD requirement: Google must confirm the email is verified.
if (!profile.email || !profile.email_verified) { if (!profile.email || !profile.email_verified) {
return NextResponse.redirect(new URL("/login?error=unverified", request.url)); return NextResponse.redirect(new URL("/login?error=unverified", APP_URL));
} }
// 5. Find or create the user. // 5. Find or create the user.
@ -92,7 +94,7 @@ export async function GET(request: Request) {
await logAudit({ userId, action: "login", request, metadata: { method: "google" } }); await logAudit({ userId, action: "login", request, metadata: { method: "google" } });
const response = NextResponse.redirect( const response = NextResponse.redirect(
new URL(isNewUser ? "/onboarding" : "/", request.url), new URL(isNewUser ? "/onboarding" : "/", APP_URL),
); );
response.cookies.set("tia_session", sessionToken, { response.cookies.set("tia_session", sessionToken, {
httpOnly: true, httpOnly: true,
@ -106,6 +108,6 @@ export async function GET(request: Request) {
return response; return response;
} catch (e) { } catch (e) {
console.error("Google callback error:", e); console.error("Google callback error:", e);
return NextResponse.redirect(new URL("/login?error=oauth", request.url)); return NextResponse.redirect(new URL("/login?error=oauth", APP_URL));
} }
} }

View file

@ -4,11 +4,13 @@ import { logAudit } from "@/lib/audit";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com";
// GET — the link target from the verification email. // GET — the link target from the verification email.
export async function GET(request: Request) { export async function GET(request: Request) {
const token = new URL(request.url).searchParams.get("token"); const token = new URL(request.url).searchParams.get("token");
if (!token) { if (!token) {
return NextResponse.redirect(new URL("/login?error=verify", request.url)); return NextResponse.redirect(new URL("/login?error=verify", APP_URL));
} }
try { try {
@ -19,7 +21,7 @@ export async function GET(request: Request) {
LIMIT 1 LIMIT 1
`; `;
if (rows.length === 0) { if (rows.length === 0) {
return NextResponse.redirect(new URL("/login?error=verify", request.url)); return NextResponse.redirect(new URL("/login?error=verify", APP_URL));
} }
const verifyId = rows[0].id; const verifyId = rows[0].id;
@ -39,7 +41,7 @@ export async function GET(request: Request) {
`; `;
await logAudit({ userId, action: "email_verified", request }); await logAudit({ userId, action: "email_verified", request });
const response = NextResponse.redirect(new URL("/onboarding", request.url)); const response = NextResponse.redirect(new URL("/onboarding", APP_URL));
response.cookies.set("tia_session", sessionToken, { response.cookies.set("tia_session", sessionToken, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
@ -50,6 +52,6 @@ export async function GET(request: Request) {
return response; return response;
} catch (e) { } catch (e) {
console.error("Verify email error:", e); console.error("Verify email error:", e);
return NextResponse.redirect(new URL("/login?error=verify", request.url)); return NextResponse.redirect(new URL("/login?error=verify", APP_URL));
} }
} }