mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
fix(auth): build verify-email redirects from the configured public origin
Redirects were built relative to `request.url`, which behind a reverse proxy resolves to the container-internal address. Verification succeeded but the browser was sent to localhost:3000, so users saw a connection error instead of the "email verified" confirmation. Add getPublicOrigin() (NEXTAUTH_URL, then NEXT_PUBLIC_APP_URL, falling back to the request origin for local development) and use it for every verify-email redirect. The legacy GET redirect in the watch session route had the same defect and is fixed alongside it.
This commit is contained in:
@@ -2,11 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { consumeVerificationToken } from '@/lib/email-verification';
|
||||
import { rateLimit } from '@/lib/rate-limit';
|
||||
import { logError } from '@/lib/logger';
|
||||
import { getPublicOrigin } from '@/lib/request-origin';
|
||||
|
||||
// A raw 32-byte hex token is exactly 64 characters.
|
||||
const TOKEN_REGEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Redirect targets must be built from the public origin, not `request.url`:
|
||||
// behind a reverse proxy the latter is the container-internal address and the
|
||||
// user lands on a dead host even though verification succeeded.
|
||||
const origin = getPublicOrigin(request);
|
||||
const redirectTo = (path: string) => NextResponse.redirect(new URL(path, origin));
|
||||
|
||||
try {
|
||||
// Rate-limit by IP to prevent token enumeration attacks.
|
||||
const limited = await rateLimit(request, 'verify-email');
|
||||
@@ -15,18 +22,18 @@ export async function GET(request: NextRequest) {
|
||||
const token = request.nextUrl.searchParams.get('token');
|
||||
|
||||
if (!token || !TOKEN_REGEX.test(token.trim())) {
|
||||
return NextResponse.redirect(new URL('/login?error=InvalidVerificationToken', request.url));
|
||||
return redirectTo('/login?error=InvalidVerificationToken');
|
||||
}
|
||||
|
||||
const email = await consumeVerificationToken(token.trim());
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.redirect(new URL('/login?error=InvalidVerificationToken', request.url));
|
||||
return redirectTo('/login?error=InvalidVerificationToken');
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/login?verified=true', request.url));
|
||||
return redirectTo('/login?verified=true');
|
||||
} catch (err) {
|
||||
logError('Email verification error:', err);
|
||||
return NextResponse.redirect(new URL('/login?error=VerificationFailed', request.url));
|
||||
return redirectTo('/login?error=VerificationFailed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createHash } from 'crypto';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { checkRateLimit, getClientIp, rateLimit, rateLimitHeaders } from '@/lib/rate-limit';
|
||||
import { isTrustedSameOriginRequest } from '@/lib/request-origin';
|
||||
import { getPublicOrigin, isTrustedSameOriginRequest } from '@/lib/request-origin';
|
||||
import { MAX_SHARE_PASSWORD_LENGTH, validateShareLinkAccess } from '@/lib/share-links';
|
||||
import {
|
||||
createPendingShareValue,
|
||||
@@ -48,7 +48,7 @@ function validateSameOriginRequest(request: NextRequest): NextResponse | null {
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const { videoId } = await params;
|
||||
const cleanWatchUrl = new URL(`/watch/${videoId}`, request.nextUrl.origin);
|
||||
const cleanWatchUrl = new URL(`/watch/${videoId}`, getPublicOrigin(request));
|
||||
const legacyShareToken = request.nextUrl.searchParams.get('shareToken');
|
||||
|
||||
// Keep GET route for backwards compatibility, but never establish session from GET.
|
||||
|
||||
@@ -16,6 +16,26 @@ function getConfiguredOrigins(): string[] {
|
||||
.filter((value): value is string => value !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin to build user-facing redirects from.
|
||||
*
|
||||
* Behind a reverse proxy (Docker deployments) `request.nextUrl.origin` is the
|
||||
* container-internal address (`localhost:3000`), so redirecting relative to the
|
||||
* request URL sends the browser to a dead host. Prefer the operator-configured
|
||||
* public origin and fall back to the request origin for local development.
|
||||
*/
|
||||
export function getPublicOrigin(request: NextRequest): string {
|
||||
const configured = [process.env.NEXTAUTH_URL, process.env.NEXT_PUBLIC_APP_URL];
|
||||
|
||||
for (const value of configured) {
|
||||
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
||||
const origin = normalizeOrigin(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
||||
if (origin) return origin;
|
||||
}
|
||||
|
||||
return request.nextUrl.origin;
|
||||
}
|
||||
|
||||
export function getAllowedRequestOrigins(request: NextRequest): Set<string> {
|
||||
const origins = new Set<string>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user