diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx index 0030afe1..139449bf 100644 --- a/src/app/[orgId]/settings/general/auth-page/page.tsx +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -38,12 +38,14 @@ export default async function AuthPage(props: AuthPageProps) { let loginPage: GetLoginPageResponse | null = null; try { - const res = await internal.get>( - `/org/${orgId}/login-page`, - await authCookieHeader() - ); - if (res.status === 200) { - loginPage = res.data.data; + if (build === "saas") { + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() + ); + if (res.status === 200) { + loginPage = res.data.data; + } } } catch (error) {} @@ -59,7 +61,7 @@ export default async function AuthPage(props: AuthPageProps) { return ( - + {build === "saas" && } ); diff --git a/src/app/auth/org/[orgId]/page.tsx b/src/app/auth/org/[orgId]/page.tsx new file mode 100644 index 00000000..7de991ca --- /dev/null +++ b/src/app/auth/org/[orgId]/page.tsx @@ -0,0 +1,188 @@ +import { formatAxiosError, priv } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { redirect } from "next/navigation"; +import { pullEnv } from "@app/lib/pullEnv"; +import { LoginFormIDP } from "@app/components/LoginForm"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; +import { build } from "@server/build"; +import { headers } from "next/headers"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; +import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { Button } from "@app/components/ui/button"; +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; +import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { TierId } from "@server/lib/billing/tiers"; + +export const dynamic = "force-dynamic"; + +export default async function OrgAuthPage(props: { + params: Promise<{}>; + searchParams: Promise<{ token?: string }>; +}) { + const params = await props.params; + const searchParams = await props.searchParams; + + const env = pullEnv(); + + const authHeader = await authCookieHeader(); + + if (searchParams.token) { + return ; + } + + const getUser = cache(verifySession); + const user = await getUser({ skipCheckVerifyEmail: true }); + + const allHeaders = await headers(); + const host = allHeaders.get("host"); + + const t = await getTranslations(); + + const expectedHost = env.app.dashboardUrl.split("//")[1]; + + let redirectToUrl: string | undefined; + let loginPage: LoadLoginPageResponse | undefined; + if (host !== expectedHost) { + try { + const res = await priv.get>( + `/login-page?fullDomain=${host}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + if (!loginPage) { + console.debug( + `No login page found for host ${host}, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + let subscriptionStatus: GetOrgTierResponse | null = null; + if (build === "saas") { + try { + const getSubscription = cache(() => + priv.get>( + `/org/${loginPage!.orgId}/billing/tier` + ) + ); + const subRes = await getSubscription(); + subscriptionStatus = subRes.data.data; + } catch {} + } + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (build === "saas" && !subscribed) { + console.log( + `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + if (user) { + let redirectToken: string | undefined; + try { + const res = await priv.post< + AxiosResponse + >(`/get-session-transfer-token`, {}, authHeader); + + if (res && res.status === 200) { + const newToken = res.data.data.token; + redirectToken = newToken; + } + } catch (e) { + console.error( + formatAxiosError(e, "Failed to get transfer token") + ); + } + + if (redirectToken) { + redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; + redirect(redirectToUrl); + } + } + } else { + console.log(`Host ${host} is the same`); + redirect(env.app.dashboardUrl); + } + + let loginIdps: LoginFormIDP[] = []; + if (build === "saas") { + const idpsRes = await cache( + async () => + await priv.get>( + `/org/${loginPage!.orgId}/idp` + ) + )(); + loginIdps = idpsRes.data.data.idps.map((idp) => ({ + idpId: idp.idpId, + name: idp.name, + variant: idp.variant + })) as LoginFormIDP[]; + } + + return ( +
+
+ + {t("poweredBy")}{" "} + + {env.branding.appName || "Pangolin"} + + +
+ + + {t("orgAuthSignInTitle")} + + {loginIdps.length > 0 + ? t("orgAuthChooseIdpDescription") + : ""} + + + + {loginIdps.length > 0 ? ( + + ) : ( +
+

+ {t("orgAuthNoIdpConfigured")} +

+ + + +
+ )} +
+
+
+ ); +} diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 81ddf58e..e9e7b731 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -49,6 +49,8 @@ import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext" import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; const pinSchema = z.object({ pin: z @@ -99,6 +101,19 @@ type ResourceAuthPortalProps = { } | null; }; +/** + * TODO: remove +- Auth page domain => only in SaaS +- Branding => saas & enterprise for a paid user ? +- ... +- resource auth page: `/auth/resource/[guid]` || (auth page domain/...) +- org auth page: `/auth/org/[orgId]` + => only in SaaS + => branding org title/subtitle only in SaaS + => unauthenticated + + */ + export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const t = useTranslations(); diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts new file mode 100644 index 00000000..e5754f07 --- /dev/null +++ b/src/hooks/usePaidStatus.ts @@ -0,0 +1,18 @@ +import { build } from "@server/build"; +import { useLicenseStatusContext } from "./useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "./useSubscriptionStatusContext"; + +export function usePaidStatus() { + const { isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + // Check if features are disabled due to licensing/subscription + const isEnterpriseLicensed = build === "enterprise" && isUnlocked(); + const isSaasSubscribed = build === "saas" && subscription?.isSubscribed(); + + return { + isEnterpriseLicensed, + isSaasSubscribed, + isPaidUser: isEnterpriseLicensed || isSaasSubscribed + }; +}