Plug sign in steps + sso login URL check
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<02b9c661a564d47f4821ff67a4dae9b0>>
|
||||
* @generated SignedSource<<f242c4003408e8b86541c8f98b2c6d18>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -13,19 +13,19 @@ export type SignInInput = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
export type SignInPageMutation$variables = {
|
||||
export type PasswordSignInPageMutation$variables = {
|
||||
input: SignInInput;
|
||||
};
|
||||
export type SignInPageMutation$data = {
|
||||
export type PasswordSignInPageMutation$data = {
|
||||
readonly signIn: {
|
||||
readonly session: {
|
||||
readonly id: string;
|
||||
} | null | undefined;
|
||||
} | null | undefined;
|
||||
};
|
||||
export type SignInPageMutation = {
|
||||
response: SignInPageMutation$data;
|
||||
variables: SignInPageMutation$variables;
|
||||
export type PasswordSignInPageMutation = {
|
||||
response: PasswordSignInPageMutation$data;
|
||||
variables: PasswordSignInPageMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -78,7 +78,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SignInPageMutation",
|
||||
"name": "PasswordSignInPageMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
@@ -87,20 +87,20 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SignInPageMutation",
|
||||
"name": "PasswordSignInPageMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "8e13b6ebdfd4ca6bde158dff53f596db",
|
||||
"cacheID": "f60c1dcde17bef06ff8170d14c077dae",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SignInPageMutation",
|
||||
"name": "PasswordSignInPageMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n"
|
||||
"text": "mutation PasswordSignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "6424104df106b64a6237d92299239a63";
|
||||
(node as any).hash = "9b9b3de59d38ae7c288348c6bfc01afa";
|
||||
|
||||
export default node;
|
||||
83
apps/console/src/__generated__/iam/SSOSignInPageQuery.graphql.ts
generated
Normal file
83
apps/console/src/__generated__/iam/SSOSignInPageQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @generated SignedSource<<44e9b1516c9cd0ae8419f326791e09e1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { Result } from "relay-runtime";
|
||||
export type SSOSignInPageQuery$variables = {
|
||||
email: string;
|
||||
};
|
||||
export type SSOSignInPageQuery$data = {
|
||||
readonly ssoLoginURL: Result<string | null | undefined, unknown>;
|
||||
};
|
||||
export type SSOSignInPageQuery = {
|
||||
response: SSOSignInPageQuery$data;
|
||||
variables: SSOSignInPageQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "email"
|
||||
}
|
||||
],
|
||||
v1 = {
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "email",
|
||||
"variableName": "email"
|
||||
}
|
||||
],
|
||||
"kind": "ScalarField",
|
||||
"name": "ssoLoginURL",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SSOSignInPageQuery",
|
||||
"selections": [
|
||||
{
|
||||
"kind": "CatchField",
|
||||
"field": (v1/*: any*/),
|
||||
"to": "RESULT"
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SSOSignInPageQuery",
|
||||
"selections": [
|
||||
(v1/*: any*/)
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "9b7766bbd9f160dc8bbcf34705641486",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SSOSignInPageQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query SSOSignInPageQuery(\n $email: EmailAddr!\n) {\n ssoLoginURL(email: $email)\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "5749c48e67881c71bbc99a9e1120767f";
|
||||
|
||||
export default node;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||
import type { FormEventHandler } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { Link } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { SignInPageMutation } from "/__generated__/iam/SignInPageMutation.graphql";
|
||||
import type { PasswordSignInPageMutation } from "/__generated__/iam/PasswordSignInPageMutation.graphql";
|
||||
|
||||
const signInMutation = graphql`
|
||||
mutation SignInPageMutation($input: SignInInput!) {
|
||||
mutation PasswordSignInPageMutation($input: SignInInput!) {
|
||||
signIn(input: $input) {
|
||||
session {
|
||||
id
|
||||
@@ -16,13 +16,13 @@ const signInMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO initial screen + SAML login + after sign in URL state
|
||||
export default function SignInPage() {
|
||||
export default function PasswordSignInPage() {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const [signIn, isSigningIn] = useMutation<SignInPageMutation>(signInMutation);
|
||||
const [signIn, isSigningIn] =
|
||||
useMutation<PasswordSignInPageMutation>(signInMutation);
|
||||
|
||||
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -54,14 +54,13 @@ export default function SignInPage() {
|
||||
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||
>
|
||||
<IconChevronLeft size={20} />
|
||||
<span className="text-sm">{__("Back")}</span>
|
||||
</button> */}
|
||||
</Link>
|
||||
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login with Email")}
|
||||
133
apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx
Normal file
133
apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||
import { useEffect, useState, type FormEventHandler } from "react";
|
||||
import {
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
type PreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { SSOSignInPageQuery } from "/__generated__/iam/SSOSignInPageQuery.graphql";
|
||||
|
||||
const ssoAvailabilityQuery = graphql`
|
||||
query SSOSignInPageQuery($email: EmailAddr!) {
|
||||
ssoLoginURL(email: $email) @catch(to: RESULT)
|
||||
}
|
||||
`;
|
||||
|
||||
export default function SSOSignInPage() {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
|
||||
const [checking, setChecking] = useState(false);
|
||||
|
||||
const handleSSOCheck: FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
setChecking(true);
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("email")?.toString();
|
||||
|
||||
if (!email) return;
|
||||
|
||||
loadQuery({ email }, { fetchPolicy: "network-only" });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-4" onSubmit={handleSSOCheck}>
|
||||
<Link
|
||||
to="/auth/login"
|
||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||
>
|
||||
<IconChevronLeft size={20} />
|
||||
<span className="text-sm">{__("Back")}</span>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login with SSO")}
|
||||
</h1>
|
||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||
{__("Enter your work email to continue with SSO")}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
required
|
||||
placeholder={__("Work Email")}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Work Email")}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Button className="w-full" disabled={checking}>
|
||||
{checking ? __("Checking...") : __("Continue with SSO")}
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Don't have an account ?")}{" "}
|
||||
<Link
|
||||
to="/auth/register"
|
||||
className="underline hover:text-txt-primary"
|
||||
>
|
||||
{__("Register")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{queryRef && (
|
||||
<NavigateToSSOLoginURL
|
||||
onSSOAvailabilityCheck={setChecking}
|
||||
queryRef={queryRef}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigateToSSOLoginURL(props: {
|
||||
queryRef: PreloadedQuery<SSOSignInPageQuery>;
|
||||
onSSOAvailabilityCheck: (checking: boolean) => void;
|
||||
}) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { ssoLoginURL } = usePreloadedQuery<SSOSignInPageQuery>(
|
||||
ssoAvailabilityQuery,
|
||||
queryRef,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ssoLoginURL.ok) {
|
||||
console.log(ssoLoginURL);
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description:
|
||||
ssoLoginURL.errors[0] instanceof Error
|
||||
? ssoLoginURL.errors[0].message
|
||||
: __("SSO not available for this email domain"),
|
||||
variant: "error",
|
||||
});
|
||||
|
||||
navigate("/auth/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ssoLoginURL.value) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("SSO not available for this email domain"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = ssoLoginURL.value;
|
||||
}, [__, navigate, ssoLoginURL, toast]);
|
||||
|
||||
return null;
|
||||
}
|
||||
57
apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx
Normal file
57
apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button } from "@probo/ui";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export default function SignInPage() {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login to your account")}
|
||||
</h1>
|
||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||
{__("Choose your login method")}
|
||||
</p>
|
||||
|
||||
<Button className="w-full" to="/auth/password-login">
|
||||
{__("Login with Email")}
|
||||
</Button>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span
|
||||
className="px-4 text-xs uppercase text-txt-secondary"
|
||||
style={{ backgroundColor: "var(--color-level-0)" }}
|
||||
>
|
||||
{__("Or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="secondary" className="w-full" to="/auth/sso-login">
|
||||
{__("Login with SSO")}
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Don't have an account ?")}{" "}
|
||||
<Link to="/auth/register" className="underline hover:text-txt-primary">
|
||||
{__("Register")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-txt-secondary">
|
||||
{__("Forgot password?")}{" "}
|
||||
<Link
|
||||
to="/auth/forgot-password"
|
||||
className="underline hover:text-txt-primary"
|
||||
>
|
||||
{__("Reset password")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,17 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
Component: lazy(() => import("./pages/iam/auth/SignInPage")),
|
||||
Component: lazy(() => import("./pages/iam/auth/sign-in/SignInPage")),
|
||||
},
|
||||
{
|
||||
path: "password-login",
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/auth/sign-in/PasswordSignInPage"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "sso-login",
|
||||
Component: lazy(() => import("./pages/iam/auth/sign-in/SSOSignInPage")),
|
||||
},
|
||||
{
|
||||
path: "register",
|
||||
@@ -78,7 +88,7 @@ const routes = [
|
||||
{
|
||||
path: "signup-from-invitation",
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/auth/SignUpFromInvitationPage")
|
||||
() => import("./pages/iam/auth/SignUpFromInvitationPage"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -100,13 +110,13 @@ const routes = [
|
||||
{
|
||||
index: true,
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/memberships/MembershipsPageLoader")
|
||||
() => import("./pages/iam/memberships/MembershipsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "organizations/new",
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/organizations/NewOrganizationPage")
|
||||
() => import("./pages/iam/organizations/NewOrganizationPage"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -123,7 +133,7 @@ const routes = [
|
||||
path: "/organizations/:organizationId/employee",
|
||||
Fallback: () => "fallback employee...",
|
||||
Component: lazy(
|
||||
() => import("./pages/organizations/employee/EmployeeLayoutLoader")
|
||||
() => import("./pages/organizations/employee/EmployeeLayoutLoader"),
|
||||
),
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
children: [
|
||||
@@ -132,7 +142,7 @@ const routes = [
|
||||
// Component: () => "hello world",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/organizations/employee/EmployeeDocumentsPageLoader")
|
||||
import("./pages/organizations/employee/EmployeeDocumentsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -140,7 +150,7 @@ const routes = [
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/organizations/employee/EmployeeDocumentSignaturePageLoader")
|
||||
import("./pages/organizations/employee/EmployeeDocumentSignaturePageLoader"),
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -148,7 +158,7 @@ const routes = [
|
||||
{
|
||||
path: "/organizations/:organizationId",
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/memberships/MembershipLayoutLoader")
|
||||
() => import("./pages/iam/memberships/MembershipLayoutLoader"),
|
||||
),
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
children: [
|
||||
@@ -170,7 +180,7 @@ const routes = [
|
||||
path: "settings",
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/organizations/settings/SettingsLayout")
|
||||
() => import("./pages/iam/organizations/settings/SettingsLayout"),
|
||||
),
|
||||
children: [
|
||||
{
|
||||
@@ -183,35 +193,35 @@ const routes = [
|
||||
path: "general",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader")
|
||||
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/iam/organizations/settings/MembersPageLoader")
|
||||
import("./pages/iam/organizations/settings/MembersPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "domain",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/organizations/settings/DomainSettingsPageLoader")
|
||||
import("./pages/organizations/settings/DomainSettingsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "saml-sso",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/iam/organizations/settings/SAMLSettingsPageLoader")
|
||||
import("./pages/iam/organizations/settings/SAMLSettingsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "scim",
|
||||
Component: lazy(
|
||||
() =>
|
||||
import("./pages/iam/organizations/settings/SCIMSettingsPageLoader")
|
||||
import("./pages/iam/organizations/settings/SCIMSettingsPageLoader"),
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user