Finish preliminary email validation, merge user flow w saml/ldap

This commit is contained in:
Tuan Dang
2024-04-26 20:19:43 -07:00
parent d0cb06d875
commit 858a35812a
28 changed files with 225 additions and 104 deletions
+1 -3
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, LDAPStep, MFAStep, SAMLSSOStep } from "./components";
import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils";
export const Login = () => {
@@ -58,8 +58,6 @@ export const Login = () => {
);
case 2:
return <SAMLSSOStep setStep={setStep} />;
case 3:
return <LDAPStep setStep={setStep} />;
default:
return <div />;
}
@@ -1,24 +1,23 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = {
setStep: (step: number) => void;
};
export const LoginLDAP = () => {
const router = useRouter();
const queryParams = new URLSearchParams(window.location.search);
const passedOrgSlug = queryParams.get("organizationSlug");
const passedUsername = queryParams.get("username");
export const LDAPStep = ({ setStep }: Props) => {
const [organizationSlug, setOrganizationSlug] = useState("");
const [username, setUsername] = useState("");
const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || "");
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e: React.FormEvent) => {
e.preventDefault();
try {
@@ -42,7 +41,6 @@ export const LDAPStep = ({ setStep }: Props) => {
type: "success"
});
// redirects either to /login/sso or /signup/sso
window.open(nextUrl);
window.close();
} catch (err) {
@@ -76,6 +74,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedOrgSlug !== null}
/>
</div>
</div>
@@ -90,6 +89,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedUsername !== null}
/>
</div>
</div>
@@ -122,7 +122,7 @@ export const LDAPStep = ({ setStep }: Props) => {
<div className="mt-4 flex flex-row items-center justify-center">
<button
onClick={() => {
setStep(0);
router.push("/login");
}}
type="button"
className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
@@ -25,7 +25,7 @@ type Props = {
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
const router = useRouter();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
@@ -33,7 +33,10 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const queryParams = new URLSearchParams(window.location.search);
useEffect(() => {
if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") {
if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
@@ -42,7 +45,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
);
window.close();
}
}, [])
}, []);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -196,7 +199,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
router.push("/login/ldap");
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
@@ -1 +0,0 @@
export { LDAPStep } from "./LDAPStep";
@@ -1,5 +1,4 @@
export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
+1
View File
@@ -1,2 +1,3 @@
export { Login } from "./Login";
export { LoginLDAP } from "./LoginLDAP";
export { LoginSSO } from "./LoginSSO";
@@ -49,7 +49,6 @@ type Props = {
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { user } = useUser();
@@ -150,6 +149,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
[members, searchMemberFilter]
);
console.log("filterdUser: ", filterdUser);
return (
<div>
<Input
@@ -218,7 +219,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
variant="outline_bg"
onClick={() => onResendInvite(email)}
>
Resend Invite
Resend invite
</Button>
)}
</>
+11 -5
View File
@@ -5,7 +5,8 @@ import {
BackupPDFStep,
EmailConfirmationStep,
MergeUsersStep,
UserInfoSSOStep} from "./components";
UserInfoSSOStep
} from "./components";
type Props = {
providerAuthToken: string;
@@ -15,9 +16,8 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const { username, email, organizationName, firstName, lastName } = jwt_decode(
providerAuthToken
) as any;
const { username, email, organizationName, organizationSlug, firstName, lastName, authType } =
jwt_decode(providerAuthToken) as any;
const renderView = () => {
switch (step) {
@@ -37,7 +37,13 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
case 1:
return <EmailConfirmationStep email={email} setStep={setStep} />;
case 2:
return <MergeUsersStep username={username} />;
return (
<MergeUsersStep
username={username}
authType={authType}
organizationSlug={organizationSlug}
/>
);
case 3:
return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
@@ -6,11 +6,11 @@ import ReactCodeInput from "react-code-input";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useUser } from "@app/context";
import {
fetchUsersWithMyEmail,
useSendEmailVerificationCode,
useVerifyEmailVerificationCode} from "@app/hooks/api";
useVerifyEmailVerificationCode
} from "@app/hooks/api";
type Props = {
email: string;
@@ -56,7 +56,6 @@ const propsPhone = {
} as const;
export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const { user } = useUser();
const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false);
@@ -106,9 +105,6 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
<p className="text-md flex justify-center text-bunker-200">
We&apos;ve sent a verification code to {email}
</p>
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">
{user?.email}
</p>
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
<ReactCodeInput
name=""
@@ -1,3 +1,4 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { faUsers } from "@fortawesome/free-solid-svg-icons";
@@ -5,6 +6,8 @@ import { createNotification } from "@app/components/notifications";
import {
Button,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
@@ -15,28 +18,49 @@ import {
Tr
} from "@app/components/v2";
import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
type Props = {
username: string;
authType?: UserAliasType;
organizationSlug: string;
};
export const MergeUsersStep = ({ username }: Props) => {
export const MergeUsersStep = ({ username, authType, organizationSlug }: Props) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [targetUsername, setTargetUsername] = useState("");
const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail();
const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers();
const handleMergeUser = async (targetUsername: string) => {
const handleMergeUser = async (mergeWithUsername: string) => {
try {
console.log("merge A");
await mergeUser({ username: targetUsername });
// TODO: logout, make user re-login
console.log("merge B");
if (!mergeWithUsername) return;
await mergeUser({ username: mergeWithUsername });
createNotification({
text: "Successfully merged user",
type: "success"
});
router.push("/login");
setIsOpen(false);
switch (authType) {
case UserAliasType.SAML: {
window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`);
window.close();
break;
}
case UserAliasType.LDAP: {
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
default: {
router.push("/login");
break;
}
}
setTargetUsername("");
} catch (err) {
console.error(err);
createNotification({
@@ -72,14 +96,16 @@ export const MergeUsersStep = ({ username }: Props) => {
return (
<Tr className="h-10 items-center" key={`same-email-user-${user.id}`}>
<Td>{`${user.firstName ?? ""} ${user.lastName ?? ""}`}</Td>
<Td>{username}</Td>
<Td>{user.username}</Td>
<Td>
<Button
isLoading={isLoadingMerge}
colorSchema="primary"
variant="outline_bg"
type="submit"
onClick={() => handleMergeUser(user.username)}
onClick={() => {
setIsOpen(true);
setTargetUsername(user.username);
}}
>
Merge
</Button>
@@ -97,6 +123,35 @@ export const MergeUsersStep = ({ username }: Props) => {
</TBody>
</Table>
</TableContainer>
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<ModalContent title="Merge User Confirmation">
<p className="mb-4 text-bunker-300">
The merge operation will transfer / consolidate your existing organization membership to
the target user you&apos;re merging with.
</p>
<p className="mb-4 text-bunker-300">
If the target user is not yet part of the same organization, then they will be added to
it under your current organization membership. Conversely, if the target user is already
part of the organization, then their existing organization membership will remain.
</p>
<p className="text-bunker-300">
Once the merge operation is complete, you&apos;ll be prompted to re-login.
</p>
<div className="mt-8 flex items-center">
<Button
isLoading={isLoadingMerge}
colorSchema="primary"
onClick={async () => handleMergeUser(targetUsername)}
className="mr-4"
>
Confirm
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => setIsOpen(false)}>
Cancel
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};
@@ -17,6 +17,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { sendEmailVerificationCode } from "@app/hooks/api/users/mutation";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
@@ -205,10 +206,11 @@ export const UserInfoSSOStep = ({
if (email) {
// move to verify email
await sendEmailVerificationCode();
setStep(1);
} else {
// move to backup PDF step
setStep(2);
setStep(3);
}
} catch (error) {
setIsLoading(false);