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
View File
@@ -6,6 +6,7 @@ export const publicPaths = [
"/signup",
"/signup/sso",
"/login",
"/login/ldap",
"/blog",
"/docs",
"/changelog",
+4 -2
View File
@@ -3,7 +3,8 @@ export {
useAddUserToWsNonE2EE,
useMergeUsers,
useSendEmailVerificationCode,
useVerifyEmailVerificationCode} from "./mutation";
useVerifyEmailVerificationCode
} from "./mutation";
export {
fetchOrgUsers,
fetchUsersWithMyEmail,
@@ -26,4 +27,5 @@ export {
useRevokeMySessions,
useUpdateMfaEnabled,
useUpdateOrgUserRole,
useUpdateUserAuthMethods} from "./queries";
useUpdateUserAuthMethods
} from "./queries";
+23 -1
View File
@@ -5,6 +5,7 @@ import {
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries";
@@ -63,10 +64,14 @@ export const useAddUserToWsNonE2EE = () => {
});
};
export const sendEmailVerificationCode = async () => {
return apiRequest.post("/api/v2/users/me/emails/code");
};
export const useSendEmailVerificationCode = () => {
return useMutation({
mutationFn: async () => {
await apiRequest.post("/api/v2/users/me/emails/code");
await sendEmailVerificationCode();
return {};
}
});
@@ -88,12 +93,29 @@ export const useVerifyEmailVerificationCode = () => {
};
export const useMergeUsers = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ username }: { username: string }) => {
const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", {
username
});
return data;
},
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
queryClient.clear();
}
});
};
+5
View File
@@ -27,6 +27,11 @@ export type User = {
id: string;
};
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
}
export type UserEnc = {
encryptionVersion?: number;
protectedKey?: string;
-1
View File
@@ -7,7 +7,6 @@ import { Login } from "@app/views/Login";
export default function LoginPage() {
const { t } = useTranslation();
return (
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
<Head>
+27
View File
@@ -0,0 +1,27 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { LoginLDAP } from "@app/views/Login";
export default function LoginLDAPPage() {
const { t } = useTranslation();
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<LoginLDAP />
</div>
);
}
+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);