Fix magic link routes

This commit is contained in:
adilallo
2026-05-23 18:19:45 -06:00
parent bb26d95b32
commit b84d80c3a9
7 changed files with 36 additions and 6 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"title": "Community Rule",
"author": "MEDLab",
"description": "Community governance and rule-building app",
"version": "0.1.5",
"version": "0.1.6",
"httpPort": 3000,
"healthCheckPath": "/api/health",
"memoryLimit": 805306368,
+2 -1
View File
@@ -20,6 +20,7 @@ import {
import { logRouteError } from "../../../../../lib/server/requestId";
import { apiRoute } from "../../../../../lib/server/apiRoute";
import { safeInternalPath } from "../../../../../lib/safeInternalPath";
import { getPublicOrigin } from "../../../../../lib/server/publicOrigin";
import { magicLinkRequestBodySchema } from "../../../../../lib/server/validation/createFlowSchemas";
import { jsonFromZodError } from "../../../../../lib/server/validation/zodHttp";
@@ -99,7 +100,7 @@ export const POST = apiRoute(SCOPE, async (request: NextRequest, _ctx, { request
},
});
const origin = request.nextUrl.origin;
const origin = getPublicOrigin(request);
const verifyUrl = `${origin}/api/auth/magic-link/verify?token=${encodeURIComponent(token)}`;
try {
@@ -21,6 +21,7 @@ import {
} from "../../../../../../../lib/server/responses";
import { getSessionUser } from "../../../../../../../lib/server/session";
import { rateLimitKey } from "../../../../../../../lib/server/rateLimit";
import { getPublicOrigin } from "../../../../../../../lib/server/publicOrigin";
type RouteContext = { params: Promise<{ id: string; stakeholderId: string }> };
@@ -92,7 +93,7 @@ export const POST = apiRoute<RouteContext>(
},
});
const verifyUrl = stakeholderInviteVerifyUrl(request.nextUrl.origin, token);
const verifyUrl = stakeholderInviteVerifyUrl(getPublicOrigin(request), token);
try {
await sendRuleStakeholderInviteEmail(row.email, verifyUrl, row.rule.title);
} catch (err) {
+2 -1
View File
@@ -18,6 +18,7 @@ import {
unauthorized,
} from "../../../../../lib/server/responses";
import { getSessionUser } from "../../../../../lib/server/session";
import { getPublicOrigin } from "../../../../../lib/server/publicOrigin";
import {
MAX_STAKEHOLDER_EMAILS,
postRuleStakeholderBodySchema,
@@ -151,7 +152,7 @@ export const POST = apiRoute<RouteContext>(
return serverMisconfigured();
}
const origin = request.nextUrl.origin;
const origin = getPublicOrigin(request);
const sent = await createRuleStakeholderInviteAndSendMail({
scope: "rules.stakeholders.add",
requestId,
+2 -1
View File
@@ -20,6 +20,7 @@ import { stakeholderInviteVerifyUrl } from "../../../lib/server/ruleStakeholderI
import { STAKEHOLDER_INVITE_TTL_MS } from "../../../lib/server/ruleStakeholders";
import { getSessionUser } from "../../../lib/server/session";
import { apiRoute } from "../../../lib/server/apiRoute";
import { getPublicOrigin } from "../../../lib/server/publicOrigin";
import {
publishRuleBodySchema,
uniqueStakeholderEmailsForPublish,
@@ -148,7 +149,7 @@ export const POST = apiRoute(
return { rule: created, invites: toSend };
});
const origin = request.nextUrl.origin;
const origin = getPublicOrigin(request);
try {
for (const inv of invites) {
const verifyUrl = stakeholderInviteVerifyUrl(origin, inv.token);
+2 -1
View File
@@ -20,6 +20,7 @@ import {
unauthorized,
} from "../../../../../lib/server/responses";
import { getSessionUser } from "../../../../../lib/server/session";
import { getPublicOrigin } from "../../../../../lib/server/publicOrigin";
import { readLimitedJson } from "../../../../../lib/server/validation/requestBody";
import { emailChangeRequestBodySchema } from "../../../../../lib/server/validation/userEmailChangeSchemas";
import { jsonFromZodError } from "../../../../../lib/server/validation/zodHttp";
@@ -115,7 +116,7 @@ export const POST = apiRoute(SCOPE, async (request: NextRequest, _ctx, { request
},
});
const origin = request.nextUrl.origin;
const origin = getPublicOrigin(request);
const verifyUrl = `${origin}/api/user/email-change/verify?token=${encodeURIComponent(token)}`;
try {
+25
View File
@@ -0,0 +1,25 @@
import type { NextRequest } from "next/server";
/**
* Resolve the public origin of the request, preferring proxy-forwarded headers.
*
* Next.js standalone behind Cloudron's reverse proxy sees `Host: 0.0.0.0:3000`
* (the bind address from `ENV HOSTNAME` in the Dockerfile), so
* `request.nextUrl.origin` returns an internal address unsuitable for
* outbound URLs (magic-link emails, stakeholder invites, etc.).
*
* Trusts `X-Forwarded-Host` + `X-Forwarded-Proto` when present. Falls back
* to `request.nextUrl.origin` for local dev where no proxy is in front.
*/
export function getPublicOrigin(request: NextRequest): string {
const forwardedHost = request.headers.get("x-forwarded-host");
const forwardedProto = request.headers.get("x-forwarded-proto");
if (forwardedHost) {
const proto = forwardedProto?.split(",")[0]?.trim() || "https";
const host = forwardedHost.split(",")[0]?.trim();
if (host) return `${proto}://${host}`;
}
return request.nextUrl.origin;
}