mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
add internal redirect
This commit is contained in:
@@ -18,6 +18,7 @@ import { build } from "@server/build";
|
|||||||
import OrgPolicyResult from "@app/components/OrgPolicyResult";
|
import OrgPolicyResult from "@app/components/OrgPolicyResult";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
|
import ApplyInternalRedirect from "@app/components/ApplyInternalRedirect";
|
||||||
|
|
||||||
export default async function OrgLayout(props: {
|
export default async function OrgLayout(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -70,6 +71,7 @@ export default async function OrgLayout(props: {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
|
<ApplyInternalRedirect orgId={orgId} />
|
||||||
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
||||||
<OrgPolicyResult
|
<OrgPolicyResult
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
@@ -104,6 +106,7 @@ export default async function OrgLayout(props: {
|
|||||||
env={env.app.environment}
|
env={env.app.environment}
|
||||||
sandbox_mode={env.app.sandbox_mode}
|
sandbox_mode={env.app.sandbox_mode}
|
||||||
>
|
>
|
||||||
|
<ApplyInternalRedirect orgId={orgId} />
|
||||||
{props.children}
|
{props.children}
|
||||||
<SetLastOrgCookie orgId={orgId} />
|
<SetLastOrgCookie orgId={orgId} />
|
||||||
</SubscriptionStatusProvider>
|
</SubscriptionStatusProvider>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import Script from "next/script";
|
|||||||
import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
|
import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
|
||||||
import { TailwindIndicator } from "@app/components/TailwindIndicator";
|
import { TailwindIndicator } from "@app/components/TailwindIndicator";
|
||||||
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
|
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
|
||||||
|
import StoreInternalRedirect from "@app/components/StoreInternalRedirect";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
||||||
@@ -79,6 +80,7 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html suppressHydrationWarning lang={locale}>
|
<html suppressHydrationWarning lang={locale}>
|
||||||
<body className={`${font.className} h-screen-safe overflow-hidden`}>
|
<body className={`${font.className} h-screen-safe overflow-hidden`}>
|
||||||
|
<StoreInternalRedirect />
|
||||||
<TopLoader />
|
<TopLoader />
|
||||||
{build === "saas" && (
|
{build === "saas" && (
|
||||||
<Script
|
<Script
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import OrganizationLanding from "@app/components/OrganizationLanding";
|
|||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
|
import RedirectToOrg from "@app/components/RedirectToOrg";
|
||||||
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
@@ -80,15 +81,16 @@ export default async function Page(props: {
|
|||||||
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
|
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
|
||||||
|
|
||||||
const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie);
|
const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie);
|
||||||
|
let targetOrgId: string | null = null;
|
||||||
if (lastOrgExists && lastOrgCookie) {
|
if (lastOrgExists && lastOrgCookie) {
|
||||||
redirect(`/${lastOrgCookie}`);
|
targetOrgId = lastOrgCookie;
|
||||||
} else {
|
} else {
|
||||||
let ownedOrg = orgs.find((org) => org.isOwner);
|
let ownedOrg = orgs.find((org) => org.isOwner);
|
||||||
if (!ownedOrg) {
|
if (!ownedOrg) {
|
||||||
ownedOrg = orgs[0];
|
ownedOrg = orgs[0];
|
||||||
}
|
}
|
||||||
if (ownedOrg) {
|
if (ownedOrg) {
|
||||||
redirect(`/${ownedOrg.orgId}`);
|
targetOrgId = ownedOrg.orgId;
|
||||||
} else {
|
} else {
|
||||||
if (!env.flags.disableUserCreateOrg || user.serverAdmin) {
|
if (!env.flags.disableUserCreateOrg || user.serverAdmin) {
|
||||||
redirect("/setup");
|
redirect("/setup");
|
||||||
@@ -96,6 +98,10 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetOrgId) {
|
||||||
|
return <RedirectToOrg targetOrgId={targetOrgId} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgs={orgs} navItems={[]}>
|
<Layout orgs={orgs} navItems={[]}>
|
||||||
|
|||||||
24
src/components/ApplyInternalRedirect.tsx
Normal file
24
src/components/ApplyInternalRedirect.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { consumeInternalRedirectPath } from "@app/lib/internalRedirect";
|
||||||
|
|
||||||
|
type ApplyInternalRedirectProps = {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ApplyInternalRedirect({
|
||||||
|
orgId
|
||||||
|
}: ApplyInternalRedirectProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const path = consumeInternalRedirectPath();
|
||||||
|
if (path) {
|
||||||
|
router.replace(`/${orgId}${path}`);
|
||||||
|
}
|
||||||
|
}, [orgId, router]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
24
src/components/RedirectToOrg.tsx
Normal file
24
src/components/RedirectToOrg.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { getInternalRedirectTarget } from "@app/lib/internalRedirect";
|
||||||
|
|
||||||
|
type RedirectToOrgProps = {
|
||||||
|
targetOrgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RedirectToOrg({ targetOrgId }: RedirectToOrgProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const target = getInternalRedirectTarget(targetOrgId);
|
||||||
|
router.replace(target);
|
||||||
|
} catch {
|
||||||
|
router.replace(`/${targetOrgId}`);
|
||||||
|
}
|
||||||
|
}, [targetOrgId, router]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
27
src/components/StoreInternalRedirect.tsx
Normal file
27
src/components/StoreInternalRedirect.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { INTERNAL_REDIRECT_KEY } from "@app/lib/internalRedirect";
|
||||||
|
|
||||||
|
const TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
|
|
||||||
|
export default function StoreInternalRedirect() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const value = params.get("internal_redirect");
|
||||||
|
if (value != null && value !== "") {
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
path: value,
|
||||||
|
expiresAt: Date.now() + TTL_MS
|
||||||
|
});
|
||||||
|
window.localStorage.setItem(INTERNAL_REDIRECT_KEY, payload);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
48
src/lib/internalRedirect.ts
Normal file
48
src/lib/internalRedirect.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
|
export const INTERNAL_REDIRECT_KEY = "internal_redirect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the internal_redirect value from localStorage if present and valid
|
||||||
|
* (within TTL). Removes it from storage. Returns the path segment (with leading
|
||||||
|
* slash) to append to an orgId, or null if none/expired/invalid.
|
||||||
|
*/
|
||||||
|
export function consumeInternalRedirectPath(): string | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(INTERNAL_REDIRECT_KEY);
|
||||||
|
if (raw == null || raw === "") return null;
|
||||||
|
|
||||||
|
window.localStorage.removeItem(INTERNAL_REDIRECT_KEY);
|
||||||
|
|
||||||
|
const { path: storedPath, expiresAt } = JSON.parse(raw) as {
|
||||||
|
path?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
typeof storedPath !== "string" ||
|
||||||
|
storedPath === "" ||
|
||||||
|
typeof expiresAt !== "number" ||
|
||||||
|
Date.now() > expiresAt
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = cleanRedirect(storedPath, { fallback: "" });
|
||||||
|
if (!cleaned) return null;
|
||||||
|
|
||||||
|
return cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full redirect target for an org: either `/${orgId}` or
|
||||||
|
* `/${orgId}${path}` if a valid internal_redirect was stored. Consumes the
|
||||||
|
* stored value.
|
||||||
|
*/
|
||||||
|
export function getInternalRedirectTarget(orgId: string): string {
|
||||||
|
const path = consumeInternalRedirectPath();
|
||||||
|
return path ? `/${orgId}${path}` : `/${orgId}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user