mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 01:29:38 +00:00
Finish preliminary email validation, merge user flow w saml/ldap
This commit is contained in:
@@ -6,6 +6,7 @@ export const publicPaths = [
|
||||
"/signup",
|
||||
"/signup/sso",
|
||||
"/login",
|
||||
"/login/ldap",
|
||||
"/blog",
|
||||
"/docs",
|
||||
"/changelog",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -27,6 +27,11 @@ export type User = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export enum UserAliasType {
|
||||
LDAP = "ldap",
|
||||
SAML = "saml"
|
||||
}
|
||||
|
||||
export type UserEnc = {
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
+11
-11
@@ -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,2 +1,3 @@
|
||||
export { Login } from "./Login";
|
||||
export { LoginLDAP } from "./LoginLDAP";
|
||||
export { LoginSSO } from "./LoginSSO";
|
||||
|
||||
+3
-2
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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}`} />
|
||||
|
||||
+2
-6
@@ -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'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'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'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);
|
||||
|
||||
Reference in New Issue
Block a user