Implement redirect-path for password method
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { AssumptionRequiredError, UnAuthenticatedError } from "@probo/relay";
|
import { AssumptionRequiredError, UnAuthenticatedError } from "@probo/relay";
|
||||||
import { Navigate, useLocation, useRouteError } from "react-router";
|
import { Navigate, useRouteError } from "react-router";
|
||||||
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
@@ -7,15 +7,26 @@ import { PageError } from "./PageError";
|
|||||||
|
|
||||||
export function OrganizationErrorBoundary() {
|
export function OrganizationErrorBoundary() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
const location = useLocation();
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
|
||||||
|
const search = new URLSearchParams([
|
||||||
|
["organization-id", organizationId],
|
||||||
|
["redirect-path", window.location.href],
|
||||||
|
]);
|
||||||
|
|
||||||
if (error instanceof UnAuthenticatedError) {
|
if (error instanceof UnAuthenticatedError) {
|
||||||
return <Navigate to="/auth/login" state={{ from: location.pathname }} />;
|
return <Navigate to={{ pathname: "/auth/login", search: "?" + search.toString() }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof AssumptionRequiredError) {
|
if (error instanceof AssumptionRequiredError) {
|
||||||
return <Navigate to={`/organizations/${organizationId}/assume`} state={{ from: location.pathname }} />;
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={{
|
||||||
|
pathname: `/organizations/${organizationId}/assume`,
|
||||||
|
search: "?" + search.toString(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
|
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { UnAuthenticatedError } from "@probo/relay";
|
import { UnAuthenticatedError } from "@probo/relay";
|
||||||
import { Navigate, useLocation, useRouteError } from "react-router";
|
import { Navigate, useRouteError } from "react-router";
|
||||||
|
|
||||||
import { PageError } from "./PageError";
|
import { PageError } from "./PageError";
|
||||||
|
|
||||||
export function RootErrorBoundary() {
|
export function RootErrorBoundary() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
const location = useLocation();
|
|
||||||
|
const search = new URLSearchParams([["redirect-path", window.location.href]]);
|
||||||
|
|
||||||
if (error instanceof UnAuthenticatedError) {
|
if (error instanceof UnAuthenticatedError) {
|
||||||
return <Navigate to="/auth/login" state={{ from: location.pathname }} />;
|
return <Navigate to={{ pathname: "/auth/login", search: "?" + search.toString() }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
|
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { useAssumeMutation } from "#/__generated__/iam/useAssumeMutation.gr
|
|||||||
import { useOrganizationId } from "../useOrganizationId";
|
import { useOrganizationId } from "../useOrganizationId";
|
||||||
|
|
||||||
interface UseAssumeParameters {
|
interface UseAssumeParameters {
|
||||||
|
afterAssumePath: string;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ const assumeMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function useAssume(params: UseAssumeParameters) {
|
export function useAssume(params: UseAssumeParameters) {
|
||||||
const { onSuccess } = params;
|
const { afterAssumePath, onSuccess } = params;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -55,7 +56,12 @@ export function useAssume(params: UseAssumeParameters) {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error instanceof UnAuthenticatedError) {
|
if (error instanceof UnAuthenticatedError) {
|
||||||
void navigate("/auth/login");
|
const search = new URLSearchParams([
|
||||||
|
["organization-id", organizationId],
|
||||||
|
["redirect-path", afterAssumePath],
|
||||||
|
]);
|
||||||
|
|
||||||
|
void navigate({ pathname: "/auth/login", search: "?" + search.toString() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -69,7 +75,9 @@ export function useAssume(params: UseAssumeParameters) {
|
|||||||
|
|
||||||
switch (result.__typename) {
|
switch (result.__typename) {
|
||||||
case "PasswordRequired":
|
case "PasswordRequired":
|
||||||
search.set("organizationId", organizationId);
|
search.set("organization-id", organizationId);
|
||||||
|
search.set("redirect-path", afterAssumePath);
|
||||||
|
|
||||||
void navigate({ pathname: "/auth/passord-login", search: "?" + search.toString() });
|
void navigate({ pathname: "/auth/passord-login", search: "?" + search.toString() });
|
||||||
break;
|
break;
|
||||||
case "SAMLAuthenticationRequired":
|
case "SAMLAuthenticationRequired":
|
||||||
@@ -80,7 +88,7 @@ export function useAssume(params: UseAssumeParameters) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [onSuccess, navigate, assumeOrganizationSession, organizationId]);
|
}, [afterAssumePath, organizationId, onSuccess, navigate, assumeOrganizationSession]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function PasswordSignInPage() {
|
|||||||
email: emailValue,
|
email: emailValue,
|
||||||
password: passwordValue,
|
password: passwordValue,
|
||||||
// Assume when signing in
|
// Assume when signing in
|
||||||
organizationId: searchParams.get("organizationId"),
|
organizationId: searchParams.get("organization-id"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCompleted: (_, error) => {
|
onCompleted: (_, error) => {
|
||||||
@@ -57,7 +57,7 @@ export default function PasswordSignInPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = "/";
|
window.location.href = searchParams.get("redirect-path") ?? "/";
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button } from "@probo/ui";
|
import { Button } from "@probo/ui";
|
||||||
import { Link } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<h1 className="text-center text-2xl font-bold">
|
<h1 className="text-center text-2xl font-bold">
|
||||||
@@ -14,7 +16,10 @@ export default function SignInPage() {
|
|||||||
{__("Choose your login method")}
|
{__("Choose your login method")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="w-xs h-10 mx-auto" to="/auth/password-login">
|
<Button
|
||||||
|
className="w-xs h-10 mx-auto"
|
||||||
|
to={{ pathname: "/auth/password-login", search: location.search }}
|
||||||
|
>
|
||||||
{__("Login with Email")}
|
{__("Login with Email")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -27,14 +32,21 @@ export default function SignInPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="secondary" className="w-xs h-10 mx-auto" to="/auth/sso-login">
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="w-xs h-10 mx-auto"
|
||||||
|
to={{ pathname: "/auth/sso-login", search: location.search }}
|
||||||
|
>
|
||||||
{__("Login with SSO")}
|
{__("Login with SSO")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}
|
{__("Don't have an account ?")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link to="/auth/register" className="underline hover:text-txt-primary">
|
<Link
|
||||||
|
to={{ pathname: "/auth/register", search: location.search }}
|
||||||
|
className="underline hover:text-txt-primary"
|
||||||
|
>
|
||||||
{__("Register")}
|
{__("Register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { useLocation, useNavigate } from "react-router";
|
import { useNavigate, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import { useAssume } from "#/hooks/iam/useAssume";
|
import { useAssume } from "#/hooks/iam/useAssume";
|
||||||
|
import { IAMRelayProvider } from "#/providers/IAMRelayProvider";
|
||||||
|
|
||||||
import AuthLayout from "../auth/AuthLayout";
|
import AuthLayout from "../auth/AuthLayout";
|
||||||
|
|
||||||
interface State {
|
function AssumePageInner() {
|
||||||
from: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AssumePage() {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const [searchParams] = useSearchParams();
|
||||||
const state = location.state as State;
|
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
useAssume({
|
useAssume({
|
||||||
onSuccess: () => void navigate(state.from),
|
afterAssumePath: searchParams.get("redirect-path") ?? "/",
|
||||||
|
onSuccess: () => void navigate(searchParams.get("redirect-path") ?? "/"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,3 +29,11 @@ export default function AssumePage() {
|
|||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function AssumePage() {
|
||||||
|
return (
|
||||||
|
<IAMRelayProvider>
|
||||||
|
<AssumePageInner />
|
||||||
|
</IAMRelayProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Skeleton } from "@probo/ui";
|
import { Skeleton } from "@probo/ui";
|
||||||
import { Suspense, useCallback } from "react";
|
import { Suspense, useCallback } from "react";
|
||||||
import { useQueryLoader } from "react-relay";
|
import { useQueryLoader } from "react-relay";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
import type { ViewerMembershipLayoutQuery } from "#/__generated__/iam/ViewerMembershipLayoutQuery.graphql";
|
import type { ViewerMembershipLayoutQuery } from "#/__generated__/iam/ViewerMembershipLayoutQuery.graphql";
|
||||||
import { useAssume } from "#/hooks/iam/useAssume";
|
import { useAssume } from "#/hooks/iam/useAssume";
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
|
|
||||||
function ViewerMembershipLayoutQueryLoader() {
|
function ViewerMembershipLayoutQueryLoader() {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [queryRef, loadQuery] = useQueryLoader<ViewerMembershipLayoutQuery>(
|
const [queryRef, loadQuery] = useQueryLoader<ViewerMembershipLayoutQuery>(
|
||||||
viewerMembershipLayoutQuery,
|
viewerMembershipLayoutQuery,
|
||||||
@@ -28,7 +30,10 @@ function ViewerMembershipLayoutQueryLoader() {
|
|||||||
[loadQuery, organizationId],
|
[loadQuery, organizationId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useAssume({ onSuccess: onAssumeSuccess });
|
useAssume({
|
||||||
|
afterAssumePath: location.pathname,
|
||||||
|
onSuccess: onAssumeSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <Skeleton className="w-full h-screen" />;
|
return <Skeleton className="w-full h-screen" />;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Skeleton } from "@probo/ui";
|
import { Skeleton } from "@probo/ui";
|
||||||
import { Suspense, useCallback } from "react";
|
import { Suspense, useCallback } from "react";
|
||||||
import { useQueryLoader } from "react-relay";
|
import { useQueryLoader } from "react-relay";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
import type { ViewerMembershipLayoutQuery } from "#/__generated__/iam/ViewerMembershipLayoutQuery.graphql";
|
import type { ViewerMembershipLayoutQuery } from "#/__generated__/iam/ViewerMembershipLayoutQuery.graphql";
|
||||||
import { useAssume } from "#/hooks/iam/useAssume";
|
import { useAssume } from "#/hooks/iam/useAssume";
|
||||||
@@ -17,6 +18,7 @@ function EmployeeLayoutQueryLoader() {
|
|||||||
const [queryRef, loadQuery] = useQueryLoader<ViewerMembershipLayoutQuery>(
|
const [queryRef, loadQuery] = useQueryLoader<ViewerMembershipLayoutQuery>(
|
||||||
viewerMembershipLayoutQuery,
|
viewerMembershipLayoutQuery,
|
||||||
);
|
);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const onAssumeSuccess = useCallback(
|
const onAssumeSuccess = useCallback(
|
||||||
() =>
|
() =>
|
||||||
@@ -27,7 +29,10 @@ function EmployeeLayoutQueryLoader() {
|
|||||||
[loadQuery, organizationId],
|
[loadQuery, organizationId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useAssume({ onSuccess: onAssumeSuccess });
|
useAssume({
|
||||||
|
afterAssumePath: location.pathname,
|
||||||
|
onSuccess: onAssumeSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <Skeleton className="w-full h-screen" />;
|
return <Skeleton className="w-full h-screen" />;
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/organizations/:organizationId",
|
path: "/organizations/:organizationId",
|
||||||
|
ErrorBoundary: OrganizationErrorBoundary,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "assume",
|
path: "assume",
|
||||||
@@ -132,7 +133,6 @@ const routes = [
|
|||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/organizations/employee/EmployeeLayoutLoader"),
|
() => import("./pages/organizations/employee/EmployeeLayoutLoader"),
|
||||||
),
|
),
|
||||||
ErrorBoundary: OrganizationErrorBoundary,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
@@ -154,9 +154,9 @@ const routes = [
|
|||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/organizations/ViewerMembershipLayoutLoader"),
|
() => import("./pages/iam/organizations/ViewerMembershipLayoutLoader"),
|
||||||
),
|
),
|
||||||
ErrorBoundary: OrganizationErrorBoundary,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
path: "",
|
||||||
Component: () => {
|
Component: () => {
|
||||||
const { role } = use(CurrentUser);
|
const { role } = use(CurrentUser);
|
||||||
switch (role) {
|
switch (role) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
FC,
|
FC,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Link } from "react-router";
|
import { Link, type To } from "react-router";
|
||||||
import { tv, type VariantProps } from "tailwind-variants";
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
import { Slot } from "../Slot";
|
import { Slot } from "../Slot";
|
||||||
@@ -49,7 +49,7 @@ type Props = PropsWithChildren<
|
|||||||
| "tertiary"
|
| "tertiary"
|
||||||
| "quaternary"
|
| "quaternary"
|
||||||
| "danger";
|
| "danger";
|
||||||
to?: string;
|
to?: To;
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
} & VariantProps<typeof button>
|
} & VariantProps<typeof button>
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -321,12 +321,11 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
|||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
) (*coredata.Session, *coredata.Membership, error) {
|
) (*coredata.Session, *coredata.Membership, error) {
|
||||||
var (
|
var (
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
rootSession = &coredata.Session{}
|
rootSession = &coredata.Session{}
|
||||||
identity = &coredata.Identity{}
|
identity = &coredata.Identity{}
|
||||||
membership = &coredata.Membership{}
|
membership = &coredata.Membership{}
|
||||||
childSession = &coredata.Session{}
|
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
err := s.pg.WithTx(
|
err := s.pg.WithTx(
|
||||||
@@ -366,7 +365,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
|||||||
}
|
}
|
||||||
|
|
||||||
tenantID := scope.GetTenantID()
|
tenantID := scope.GetTenantID()
|
||||||
childSession = &coredata.Session{
|
childSession := &coredata.Session{
|
||||||
ID: gid.New(tenantID, coredata.SessionEntityType),
|
ID: gid.New(tenantID, coredata.SessionEntityType),
|
||||||
IdentityID: rootSession.IdentityID,
|
IdentityID: rootSession.IdentityID,
|
||||||
TenantID: &tenantID,
|
TenantID: &tenantID,
|
||||||
@@ -399,7 +398,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return childSession, membership, nil
|
return rootSession, membership, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenSAMLChildSessionForOrganization creates a SAML-authenticated child session for the given
|
// OpenSAMLChildSessionForOrganization creates a SAML-authenticated child session for the given
|
||||||
|
|||||||
Reference in New Issue
Block a user