mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 17:08:18 +00:00
feat: ( redirect ) add redirect url based on the domain name
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Domain-aware redirect helpers.
|
||||
*
|
||||
* The passenger portal is served from more than one public domain
|
||||
* (e.g. https://bookingedr.et and https://passenger.edrsc.com). Payment
|
||||
* providers and Fayda need a browser-facing return/redirect URL, and that URL
|
||||
* must live on the SAME domain the user is currently browsing — otherwise the
|
||||
* user is bounced to the "other" site mid-flow.
|
||||
*
|
||||
* We derive the target domain from the incoming request's Origin (falling back
|
||||
* to Referer), but only trust it when it matches an explicit allowlist
|
||||
* (PAYMENT_REDIRECT_ALLOWED_ORIGINS) so a spoofed Origin can't turn these into
|
||||
* an open redirect. When the origin is absent or not allowlisted, the
|
||||
* configured (env) URL is left untouched.
|
||||
*
|
||||
* NOTE: this only rebases the ORIGIN (scheme + host + port). The path/query of
|
||||
* the configured URL is preserved, so the redirect always keeps its known
|
||||
* suffix (e.g. /booking/payment/telebirr/success). For Fayda this matters — the
|
||||
* full redirect_uri, host included, must be pre-registered with eSignet for
|
||||
* BOTH domains, or the login is rejected.
|
||||
*/
|
||||
|
||||
/** Normalize to a bare origin ("https://host[:port]"), or null if unparseable. */
|
||||
function toOrigin(value?: string | null): string | null {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return new URL(value.trim()).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Allowlisted origins from PAYMENT_REDIRECT_ALLOWED_ORIGINS (comma-separated). */
|
||||
function allowedOrigins(): string[] {
|
||||
return (process.env.PAYMENT_REDIRECT_ALLOWED_ORIGINS ?? "")
|
||||
.split(",")
|
||||
.map((o) => toOrigin(o))
|
||||
.filter((o): o is string => o !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the origin the request came from, restricted to the allowlist. Sources
|
||||
* are tried in order of trustworthiness:
|
||||
* 1. `origin` — the Origin header (most reliable on cross-origin requests).
|
||||
* 2. `referer` — Referer fallback (browsers omit Origin on some same-site
|
||||
* navigations but usually still send Referer).
|
||||
* 3. `frontendBaseUrl` — the X-Frontend-Base-URL header the portal sets
|
||||
* explicitly, used as a last resort when neither of the above is present.
|
||||
* Returns null when none is present or allowlisted — callers then keep the
|
||||
* configured URL.
|
||||
*/
|
||||
export function resolveAllowedOrigin(
|
||||
origin?: string | null,
|
||||
referer?: string | null,
|
||||
frontendBaseUrl?: string | null,
|
||||
): string | null {
|
||||
const candidate =
|
||||
toOrigin(origin) ?? toOrigin(referer) ?? toOrigin(frontendBaseUrl);
|
||||
if (!candidate) return null;
|
||||
return allowedOrigins().includes(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `configuredUrl` with its origin swapped to `allowedOrigin`, preserving
|
||||
* path, query and hash. If `allowedOrigin` is null or either URL is unparseable,
|
||||
* the configured URL is returned unchanged.
|
||||
*/
|
||||
export function rebaseUrlOrigin(
|
||||
configuredUrl: string | undefined,
|
||||
allowedOrigin: string | null,
|
||||
): string | undefined {
|
||||
if (!configuredUrl || !allowedOrigin) return configuredUrl;
|
||||
try {
|
||||
const target = new URL(configuredUrl);
|
||||
const base = new URL(allowedOrigin);
|
||||
target.protocol = base.protocol;
|
||||
target.host = base.host;
|
||||
return target.toString();
|
||||
} catch {
|
||||
return configuredUrl;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user