diff --git a/messages/en-US.json b/messages/en-US.json index e0728c94..c6087b42 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1866,10 +1866,19 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", "domainPickerSubdomainSanitized": "Subdomain sanitized", "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", - "orgAuthSignInTitle": "Sign in to the organization", + "orgAuthSignInTitle": "Organization Sign In", "orgAuthChooseIdpDescription": "Choose your identity provider to continue", "orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.", "orgAuthSignInWithPangolin": "Sign in with Pangolin", + "orgAuthSignInToOrg": "Sign in to an organization", + "orgAuthSelectOrgTitle": "Organization Sign In", + "orgAuthSelectOrgDescription": "Enter your organization ID to continue", + "orgAuthOrgIdPlaceholder": "your-organization", + "orgAuthOrgIdHelp": "Enter your organization's unique identifier", + "orgAuthSelectOrgHelp": "After entering your organization ID, you'll be taken to your organization's sign-in page where you can use SSO or your organization credentials.", + "orgAuthRememberOrgId": "Remember this organization ID", + "orgAuthBackToSignIn": "Back to standard sign in", + "orgAuthNoAccount": "Don't have an account?", "subscriptionRequiredToUse": "A subscription is required to use this feature.", "idpDisabled": "Identity providers are disabled.", "orgAuthPageDisabled": "Organization auth page is disabled.", @@ -1952,7 +1961,7 @@ "manageUserDevices": "User Devices", "manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources", "downloadClientBannerTitle": "Download Pangolin Client", - "downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately", + "downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.", "manageMachineClients": "Manage Machine Clients", "manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources", "machineClientsBannerTitle": "Servers & Automated Systems", @@ -2346,5 +2355,9 @@ "editInternalResourceDialogIcmp": "ICMP", "editInternalResourceDialogAccessControl": "Access Control", "editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.", - "editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535." + "editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.", + "orgAuthWhatsThis": "Where can I find my organization ID?", + "learnMore": "Learn more", + "backToHome": "Go back to home", + "needToSignInToOrg": "Need to use your organization's identity provider?" } diff --git a/next.config.ts b/next.config.ts index 05ed8e62..630a3416 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,7 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); const nextConfig: NextConfig = { + reactStrictMode: false, eslint: { ignoreDuringBuilds: true }, diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index bd633707..e87fe3ce 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -605,9 +605,18 @@ export async function validateOidcCallback( res.appendHeader("Set-Cookie", cookie); + let finalRedirectUrl = postAuthRedirectUrl; + if (loginPageId) { + finalRedirectUrl = `/auth/org/?redirect=${encodeURIComponent( + postAuthRedirectUrl + )}`; + } + + logger.debug("Final redirect URL", { finalRedirectUrl }); + return response(res, { data: { - redirectUrl: postAuthRedirectUrl + redirectUrl: finalRedirectUrl }, success: true, error: false, diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 273d4b7e..31c67966 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -3,6 +3,7 @@ import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; +import OrgInfoCard from "@app/components/OrgInfoCard"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -68,7 +69,10 @@ export default async function GeneralSettingsPage({ description={t("orgSettingsDescription")} /> - {children} +
+ + {children} +
diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 7816924f..3e78adc3 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -236,6 +236,7 @@ function DeleteForm({ org }: SectionFormProps) { } function GeneralSectionForm({ org }: SectionFormProps) { + const { updateOrg } = useOrgContext(); const form = useForm({ resolver: zodResolver( GeneralFormSchema.pick({ @@ -269,6 +270,11 @@ function GeneralSectionForm({ org }: SectionFormProps) { // Update organization await api.post(`/org/${org.orgId}`, reqData); + // Update the org context to reflect the change in the info card + updateOrg({ + name: data.name + }); + toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") @@ -315,22 +321,6 @@ function GeneralSectionForm({ org }: SectionFormProps) { )} /> - ( - - {t("subnet")} - - - - - - {t("subnetDescription")} - - - )} - /> diff --git a/src/app/auth/2fa/setup/page.tsx b/src/app/auth/2fa/setup/page.tsx index 944731b9..c74628cc 100644 --- a/src/app/auth/2fa/setup/page.tsx +++ b/src/app/auth/2fa/setup/page.tsx @@ -32,7 +32,6 @@ export default function Setup2FAPage() { console.log("2FA setup complete", redirect, email); if (redirect) { const cleanUrl = cleanRedirect(redirect); - console.log("Redirecting to:", cleanUrl); router.push(cleanUrl); } else { router.push("/"); diff --git a/src/app/auth/login/device/success/page.tsx b/src/app/auth/login/device/success/page.tsx index 81e62fd6..f725a867 100644 --- a/src/app/auth/login/device/success/page.tsx +++ b/src/app/auth/login/device/success/page.tsx @@ -6,6 +6,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { CheckCircle2 } from "lucide-react"; import { useTranslations } from "next-intl"; +import Link from "next/link"; export default function DeviceAuthSuccessPage() { const { env } = useEnvContext(); @@ -20,30 +21,38 @@ export default function DeviceAuthSuccessPage() { : 58; return ( - - -
- -
-
-

- {t("deviceActivation")} -

-
-
- -
- -
-

- {t("deviceConnected")} -

-

- {t("deviceAuthorizedMessage")} + <> + + +

+ +
+
+

+ {t("deviceActivation")}

-
- - + + +
+ +
+

+ {t("deviceConnected")} +

+

+ {t("deviceAuthorizedMessage")} +

+
+
+
+ + +

+ + {t("backToHome")} + +

+ ); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index f632a88c..7ef77807 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -119,6 +119,35 @@ export default async function Page(props: {

)} + + {!isInvite && build === "saas" ? ( +
+ {t("needToSignInToOrg")} + + {t("orgAuthSignInToOrg")} + +
+ ) : null} ); } + +function buildQueryString(searchParams: { + [key: string]: string | string[] | undefined; +}): string { + const params = new URLSearchParams(); + const redirect = searchParams.redirect; + const forceLogin = searchParams.forceLogin; + + if (redirect && typeof redirect === "string") { + params.set("redirect", redirect); + } + if (forceLogin && typeof forceLogin === "string") { + params.set("forceLogin", forceLogin); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} diff --git a/src/app/auth/org/[orgId]/page.tsx b/src/app/auth/org/[orgId]/page.tsx new file mode 100644 index 00000000..1958a388 --- /dev/null +++ b/src/app/auth/org/[orgId]/page.tsx @@ -0,0 +1,85 @@ +import { priv } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { LoginFormIDP } from "@app/components/LoginForm"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; +import { build } from "@server/build"; +import { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; +import { redirect } from "next/navigation"; +import OrgLoginPage from "@app/components/OrgLoginPage"; + +export const dynamic = "force-dynamic"; + +export default async function OrgAuthPage(props: { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ forceLogin?: string; redirect?: string }>; +}) { + const searchParams = await props.searchParams; + const params = await props.params; + + if (build !== "saas") { + const queryString = new URLSearchParams(searchParams as any).toString(); + redirect(`/auth/login${queryString ? `?${queryString}` : ""}`); + } + + const forceLoginParam = searchParams?.forceLogin; + const forceLogin = forceLoginParam === "true"; + const orgId = params.orgId; + + const getUser = cache(verifySession); + const user = await getUser({ skipCheckVerifyEmail: true }); + + if (user && !forceLogin) { + redirect("/"); + } + + let loginPage: LoadLoginPageResponse | undefined; + + try { + const res = await priv.get>( + `/login-page?orgId=${orgId}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + let loginIdps: LoginFormIDP[] = []; + if (build === "saas") { + const idpsRes = await priv.get>( + `/org/${orgId}/idp` + ); + + loginIdps = idpsRes.data.data.idps.map((idp) => ({ + idpId: idp.idpId, + name: idp.name, + variant: idp.variant + })) as LoginFormIDP[]; + } + + let branding: LoadLoginPageBrandingResponse | null = null; + if (build === "saas") { + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } catch (error) {} + } + + return ( + + ); +} diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/org/page.tsx similarity index 53% rename from src/app/auth/(private)/org/page.tsx rename to src/app/auth/org/page.tsx index 48e3cc37..8fc9bbb7 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/org/page.tsx @@ -13,36 +13,37 @@ import { LoadLoginPageBrandingResponse, 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 { replacePlaceholder } from "@app/lib/replacePlaceholder"; import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; +import { OrgSelectionForm } from "@app/components/OrgSelectionForm"; +import OrgLoginPage from "@app/components/OrgLoginPage"; export const dynamic = "force-dynamic"; export default async function OrgAuthPage(props: { params: Promise<{}>; - searchParams: Promise<{ token?: string }>; + searchParams: Promise<{ + token?: string; + redirect?: string; + forceLogin?: string; + }>; }) { const searchParams = await props.searchParams; + const forceLoginParam = searchParams.forceLogin; + const forceLogin = forceLoginParam === "true"; const env = pullEnv(); const authHeader = await authCookieHeader(); if (searchParams.token) { - return ; + return ( + + ); } const getUser = cache(verifySession); @@ -51,8 +52,6 @@ export default async function OrgAuthPage(props: { const allHeaders = await headers(); const host = allHeaders.get("host"); - const t = await getTranslations(); - const expectedHost = env.app.dashboardUrl.split("//")[1]; let redirectToUrl: string | undefined; @@ -84,7 +83,9 @@ export default async function OrgAuthPage(props: { redirect(env.app.dashboardUrl); } - if (user) { + console.log(user, forceLogin); + + if (user && !forceLogin) { let redirectToken: string | undefined; try { const res = await priv.post< @@ -102,13 +103,23 @@ export default async function OrgAuthPage(props: { } if (redirectToken) { - redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; + // redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; + // include redirect param if exists + redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}${ + searchParams.redirect + ? `&redirect=${encodeURIComponent( + searchParams.redirect + )}` + : "" + }`; + console.log( + `Redirecting logged in user to org auth callback: ${redirectToUrl}` + ); redirect(redirectToUrl); } } } else { - console.log(`Host ${host} is the same`); - redirect(env.app.dashboardUrl); + return ; } let loginIdps: LoginFormIDP[] = []; @@ -137,68 +148,11 @@ export default async function OrgAuthPage(props: { } return ( -
-
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- - - {branding?.logoUrl && ( -
- -
- )} - - {branding?.orgTitle - ? replacePlaceholder(branding.orgTitle, { - orgName: branding.orgName - }) - : t("orgAuthSignInTitle")} - - - {branding?.orgSubtitle - ? replacePlaceholder(branding.orgSubtitle, { - orgName: branding.orgName - }) - : loginIdps.length > 0 - ? t("orgAuthChooseIdpDescription") - : ""} - -
- - {loginIdps.length > 0 ? ( - - ) : ( -
-

- {t("orgAuthNoIdpConfigured")} -

- - - -
- )} -
-
-
+ ); } diff --git a/src/components/DashboardLoginForm.tsx b/src/components/DashboardLoginForm.tsx index e64a4c70..ccb5c497 100644 --- a/src/components/DashboardLoginForm.tsx +++ b/src/components/DashboardLoginForm.tsx @@ -17,7 +17,6 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import BrandingLogo from "@app/components/BrandingLogo"; import { useTranslations } from "next-intl"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { build } from "@server/build"; type DashboardLoginFormProps = { redirect?: string; @@ -49,14 +48,9 @@ export default function DashboardLoginForm({ ? env.branding.logo?.authPage?.height || 58 : 58; - const gradientClasses = - build === "saas" - ? "border-b border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background overflow-hidden rounded-t-lg" - : "border-b"; - return ( - +
diff --git a/src/components/OrgInfoCard.tsx b/src/components/OrgInfoCard.tsx new file mode 100644 index 00000000..cac8eb3f --- /dev/null +++ b/src/components/OrgInfoCard.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; + +type OrgInfoCardProps = {}; + +export default function OrgInfoCard({}: OrgInfoCardProps) { + const { org } = useOrgContext(); + const t = useTranslations(); + + return ( + + + + + {t("name")} + {org.org.name} + + + {t("orgId")} + {org.org.orgId} + + + {t("subnet")} + + {org.org.subnet || t("none")} + + + + + + ); +} + diff --git a/src/components/OrgLoginPage.tsx b/src/components/OrgLoginPage.tsx new file mode 100644 index 00000000..78b831bc --- /dev/null +++ b/src/components/OrgLoginPage.tsx @@ -0,0 +1,122 @@ +import { LoginFormIDP } from "@app/components/LoginForm"; +import { + LoadLoginPageBrandingResponse, + 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 { replacePlaceholder } from "@app/lib/replacePlaceholder"; +import { getTranslations } from "next-intl/server"; +import { pullEnv } from "@app/lib/pullEnv"; + +type OrgLoginPageProps = { + loginPage: LoadLoginPageResponse | undefined; + loginIdps: LoginFormIDP[]; + branding: LoadLoginPageBrandingResponse | null; + searchParams: { + redirect?: string; + forceLogin?: string; + }; +}; + +function buildQueryString(searchParams: { + redirect?: string; + forceLogin?: string; +}): string { + const params = new URLSearchParams(); + if (searchParams.redirect) { + params.set("redirect", searchParams.redirect); + } + if (searchParams.forceLogin) { + params.set("forceLogin", searchParams.forceLogin); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} + +export default async function OrgLoginPage({ + loginPage, + loginIdps, + branding, + searchParams +}: OrgLoginPageProps) { + const env = pullEnv(); + const t = await getTranslations(); + return ( +
+
+ + {t("poweredBy")}{" "} + + {env.branding.appName || "Pangolin"} + + +
+ + + {branding?.logoUrl && ( +
+ +
+ )} + + {branding?.orgTitle + ? replacePlaceholder(branding.orgTitle, { + orgName: branding.orgName + }) + : t("orgAuthSignInTitle")} + + + {branding?.orgSubtitle + ? replacePlaceholder(branding.orgSubtitle, { + orgName: branding.orgName + }) + : loginIdps.length > 0 + ? t("orgAuthChooseIdpDescription") + : ""} + +
+ + {loginIdps.length > 0 ? ( + + ) : ( +
+

+ {t("orgAuthNoIdpConfigured")} +

+ + + +
+ )} +
+
+
+ ); +} + diff --git a/src/components/OrgSelectionForm.tsx b/src/components/OrgSelectionForm.tsx new file mode 100644 index 00000000..51d84d36 --- /dev/null +++ b/src/components/OrgSelectionForm.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { Label } from "@app/components/ui/label"; +import { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useState, FormEvent, useEffect } from "react"; +import BrandingLogo from "@app/components/BrandingLogo"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useLocalStorage } from "@app/hooks/useLocalStorage"; +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; + +export function OrgSelectionForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const t = useTranslations(); + const { env } = useEnvContext(); + const { isUnlocked } = useLicenseStatusContext(); + const [storedOrgId, setStoredOrgId] = useLocalStorage( + "org-selection:org-id", + null + ); + const [rememberOrgId, setRememberOrgId] = useLocalStorage( + "org-selection:remember", + false + ); + const [orgId, setOrgId] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Prefill org ID from storage if remember is enabled + useEffect(() => { + if (rememberOrgId && storedOrgId) { + setOrgId(storedOrgId); + } + }, []); + + const logoWidth = isUnlocked() + ? env.branding.logo?.authPage?.width || 175 + : 175; + const logoHeight = isUnlocked() + ? env.branding.logo?.authPage?.height || 58 + : 58; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!orgId.trim()) return; + + setIsSubmitting(true); + const trimmedOrgId = orgId.trim(); + + // Save org ID to storage if remember is checked + if (rememberOrgId) { + setStoredOrgId(trimmedOrgId); + } else { + setStoredOrgId(null); + } + + const queryString = buildQueryString(searchParams); + const url = `/auth/org/${trimmedOrgId}${queryString}`; + console.log(url); + router.push(url); + }; + + return ( + <> + + +
+ +
+
+

+ {t("orgAuthSelectOrgDescription")} +

+
+
+ +
+
+ + setOrgId(e.target.value)} + required + disabled={isSubmitting} + /> +

+ {t("orgAuthWhatsThis")}{" "} + + {t("learnMore")} + +

+
+ +
+ { + setRememberOrgId(checked === true); + if (!checked) { + setStoredOrgId(null); + } + }} + /> +
+ + +
+
+
+ +

+ + {t("loginBack")} + +

+ + ); +} + +function buildQueryString(searchParams: URLSearchParams): string { + const params = new URLSearchParams(); + if (searchParams.get("redirect")) { + params.set("redirect", searchParams.get("redirect")!); + } + if (searchParams.get("forceLogin")) { + params.set("forceLogin", searchParams.get("forceLogin")!); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index ca051253..194d8b46 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -28,7 +28,7 @@ export function SettingsSectionForm({ className?: string; }) { return ( -
{children}
+
{children}
); } diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 34099558..67837ec6 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -117,7 +117,7 @@ function CollapsibleNavItem({ "flex items-center w-full rounded-md transition-colors", level === 0 ? "px-3 py-2" : "px-3 py-1.5", isActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" )} @@ -258,7 +258,7 @@ export function SidebarNav({ "flex items-center rounded-md transition-colors", isCollapsed ? "px-2 py-2 justify-center" : level === 0 ? "px-3 py-2" : "px-3 py-1.5", isActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" )} @@ -347,7 +347,7 @@ export function SidebarNav({ className={cn( "flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full", isActive || isChildActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" @@ -402,7 +402,7 @@ export function SidebarNav({ className={cn( "flex items-center rounded-md transition-colors px-3 py-1.5 text-sm", childIsActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground", childIsDisabled && "cursor-not-allowed opacity-60" diff --git a/src/components/private/IdpLoginButtons.tsx b/src/components/private/IdpLoginButtons.tsx index c2ec1f5b..e3af2d06 100644 --- a/src/components/private/IdpLoginButtons.tsx +++ b/src/components/private/IdpLoginButtons.tsx @@ -57,9 +57,15 @@ export default function IdpLoginButtons({ let redirectToUrl: string | undefined; try { + console.log( + "generating", + idpId, + redirect || "/", + orgId + ); const response = await generateOidcUrlProxy( idpId, - redirect || "/auth/org?gotoapp=app", + redirect || "/", orgId ); @@ -70,7 +76,6 @@ export default function IdpLoginButtons({ } const data = response.data; - console.log("Redirecting to:", data?.redirectUrl); if (data?.redirectUrl) { redirectToUrl = data.redirectUrl; } diff --git a/src/components/private/ValidateSessionTransferToken.tsx b/src/components/private/ValidateSessionTransferToken.tsx index fcb6a026..c83b61ba 100644 --- a/src/components/private/ValidateSessionTransferToken.tsx +++ b/src/components/private/ValidateSessionTransferToken.tsx @@ -12,6 +12,7 @@ import { TransferSessionResponse } from "@server/routers/auth/types"; type ValidateSessionTransferTokenParams = { token: string; + redirect?: string; }; export default function ValidateSessionTransferToken( @@ -49,7 +50,9 @@ export default function ValidateSessionTransferToken( } if (doRedirect) { - redirect(env.app.dashboardUrl); + // add redirect param to dashboardUrl if provided + const fullUrl = `${env.app.dashboardUrl}${props.redirect || ""}`; + router.push(fullUrl); } } diff --git a/src/contexts/orgContext.ts b/src/contexts/orgContext.ts index e5141bde..99cc8ff4 100644 --- a/src/contexts/orgContext.ts +++ b/src/contexts/orgContext.ts @@ -3,6 +3,7 @@ import { createContext } from "react"; export interface OrgContextType { org: GetOrgResponse; + updateOrg: (updatedOrg: Partial) => void; } const OrgContext = createContext(undefined); diff --git a/src/providers/OrgProvider.tsx b/src/providers/OrgProvider.tsx index 122e0127..34c8c7ef 100644 --- a/src/providers/OrgProvider.tsx +++ b/src/providers/OrgProvider.tsx @@ -10,15 +10,37 @@ interface OrgProviderProps { org: GetOrgResponse | null; } -export function OrgProvider({ children, org }: OrgProviderProps) { +export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) { const t = useTranslations(); - if (!org) { + if (!serverOrg) { throw new Error(t("orgErrorNoProvided")); } + const [org, setOrg] = useState(serverOrg); + + const updateOrg = (updatedOrg: Partial) => { + if (!org) { + throw new Error(t("orgErrorNoUpdate")); + } + setOrg((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + org: { + ...prev.org, + ...updatedOrg + } + }; + }); + }; + return ( - {children} + + {children} + ); }