mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: updated logic to have login sso
This commit is contained in:
@@ -151,7 +151,7 @@ export const userServiceFactory = ({
|
||||
if (duplicatedAccounts.length && myAccount) {
|
||||
await userDAL.transaction(async (tx) => {
|
||||
await userDAL.delete({ $in: { id: duplicatedAccounts?.map((el) => el.id) } }, tx);
|
||||
await userDAL.updateById(userId, { username: myAccount.username.toLowerCase() }, tx);
|
||||
await userDAL.updateById(userId, { username: (myAccount.email || myAccount.username).toLowerCase() }, tx);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,15 +37,18 @@ export const useGetUser = () =>
|
||||
queryFn: fetchUserDetails
|
||||
});
|
||||
|
||||
export const fetchUserDuplicateAccounts = async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
users: Array<User & { isMyAccount: boolean; devices: { ip: string; userAgent: string }[] }>;
|
||||
}>("/api/v1/user/duplicate-accounts");
|
||||
return data.users;
|
||||
};
|
||||
|
||||
export const useGetMyDuplicateAccount = () =>
|
||||
useQuery({
|
||||
queryKey: userKeys.getMyDuplicateAccount,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
users: Array<User & { isMyAccount: boolean; devices: { ip: string; userAgent: string }[] }>;
|
||||
}>("/api/v1/user/duplicate-accounts");
|
||||
return data.users;
|
||||
},
|
||||
staleTime: 60 * 1000, // 1 min in ms
|
||||
queryFn: fetchUserDuplicateAccounts,
|
||||
select: (users) => ({
|
||||
duplicateAccounts: users.filter((el) => !el.isMyAccount),
|
||||
myAccount: users?.find((el) => el.isMyAccount)
|
||||
|
||||
@@ -18,7 +18,8 @@ import { useToggle } from "@app/hooks";
|
||||
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||
import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries";
|
||||
import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation";
|
||||
|
||||
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
|
||||
|
||||
@@ -40,6 +41,7 @@ export const PasswordStep = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(true);
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
@@ -109,6 +111,13 @@ export const PasswordStep = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const userDuplicateAccount = await fetchUserDuplicateAccounts();
|
||||
const hasDuplicate = userDuplicateAccount?.length > 1;
|
||||
if (hasDuplicate) {
|
||||
setRemoveDuplicateLater(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateUserToOrg(navigate, organizationId);
|
||||
};
|
||||
|
||||
@@ -306,6 +315,18 @@ export const PasswordStep = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!removeDuplicateLater) {
|
||||
return (
|
||||
<EmailDuplicationConfirmation
|
||||
onRemoveDuplicateLater={() =>
|
||||
navigateUserToOrg(navigate, organizationId).catch(() =>
|
||||
createNotification({ text: "Failed to navigate user", type: "error" })
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasExchangedPrivateKey) {
|
||||
return (
|
||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useCallback } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Alert, Button, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
useGetMyDuplicateAccount,
|
||||
useLogoutUser,
|
||||
useRemoveMyDuplicateAccounts
|
||||
} from "@app/hooks/api";
|
||||
|
||||
type Props = {
|
||||
onRemoveDuplicateLater: () => void;
|
||||
};
|
||||
|
||||
export const EmailDuplicationConfirmation = ({ onRemoveDuplicateLater }: Props) => {
|
||||
const duplicateAccounts = useGetMyDuplicateAccount();
|
||||
const removeDuplicateEmails = useRemoveMyDuplicateAccounts();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const logout = useLogoutUser(true);
|
||||
const handleLogout = useCallback(async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
navigate({ to: "/login" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [logout, navigate]);
|
||||
|
||||
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">
|
||||
<Helmet>
|
||||
<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") ?? ""} />
|
||||
</Helmet>
|
||||
<div className="mx-auto mt-20 w-fit max-w-2xl rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
||||
<Link to="/">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<img
|
||||
src="/images/gradientLogo.svg"
|
||||
style={{
|
||||
height: "90px",
|
||||
width: "120px"
|
||||
}}
|
||||
alt="Infisical logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<form className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-2xl font-medium text-transparent">
|
||||
Multiple Accounts Detected
|
||||
</h1>
|
||||
<p className="text-md mb-4 text-center text-white">
|
||||
<span className="text-slate-300">Your current account is: </span>{" "}
|
||||
<b>{duplicateAccounts?.data?.myAccount?.username}</b>.
|
||||
</p>
|
||||
<Alert variant="warning" hideTitle>
|
||||
<div>
|
||||
We've detected multiple accounts using variations of the same email address.
|
||||
Confirm to retain this account <b>{duplicateAccounts?.data?.myAccount?.username}</b>
|
||||
. Upon confirmation other accounts will be <b>removed</b>.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
<div className="thin-scrollbar flex h-full max-h-60 w-full flex-col items-stretch gap-2 overflow-auto rounded-md">
|
||||
{duplicateAccounts?.data?.duplicateAccounts?.map((el) => {
|
||||
const lastSession = el.devices?.at(-1);
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex items-center gap-8 rounded-md bg-mineshaft-700 px-4 py-3 text-gray-200"
|
||||
>
|
||||
<div className="group flex flex-grow flex-col">
|
||||
<div className="truncate text-sm transition-colors">{el.username}</div>
|
||||
<div className="mt-2 text-xs">
|
||||
Last login: {format(new Date(el.updatedAt), "Pp")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<div className="flex flex-col space-y-1 text-sm">
|
||||
<div>IP: {lastSession?.ip || "-"}</div>
|
||||
<div>User Agent: {lastSession?.userAgent || "-"}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 flex w-full flex-col">
|
||||
<div className="flex gap-6">
|
||||
<Button
|
||||
className="flex-1 flex-grow"
|
||||
isLoading={removeDuplicateEmails.isPending}
|
||||
onClick={() =>
|
||||
removeDuplicateEmails.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Removed duplicate accounts"
|
||||
});
|
||||
onRemoveDuplicateLater();
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => onRemoveDuplicateLater()}
|
||||
className="flex-1 flex-grow"
|
||||
>
|
||||
Ask Again Later?
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={logout.isPending}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="mt-4"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Change Account
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="pb-28" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,9 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Alert, Button, Spinner, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
useGetMyDuplicateAccount,
|
||||
useLogoutUser,
|
||||
useRemoveMyDuplicateAccounts
|
||||
} from "@app/hooks/api";
|
||||
import { Spinner } from "@app/components/v2";
|
||||
import { useGetMyDuplicateAccount } from "@app/hooks/api";
|
||||
|
||||
import { EmailDuplicationConfirmation } from "./EmailDuplicationConfirmation";
|
||||
import { SelectOrganizationSection } from "./SelectOrgSection";
|
||||
|
||||
const LoadingScreen = () => {
|
||||
@@ -27,139 +17,17 @@ const LoadingScreen = () => {
|
||||
|
||||
export const SelectOrganizationPage = () => {
|
||||
const duplicateAccounts = useGetMyDuplicateAccount();
|
||||
const removeDuplicateEmails = useRemoveMyDuplicateAccounts();
|
||||
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const logout = useLogoutUser(true);
|
||||
const handleLogout = useCallback(async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
navigate({ to: "/login" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [logout, navigate]);
|
||||
|
||||
if (duplicateAccounts.isPending) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!duplicateAccounts.data?.duplicateAccounts?.length || removeDuplicateLater) {
|
||||
return <SelectOrganizationSection />;
|
||||
if (duplicateAccounts.data?.duplicateAccounts?.length && !removeDuplicateLater) {
|
||||
return (
|
||||
<EmailDuplicationConfirmation onRemoveDuplicateLater={() => setRemoveDuplicateLater(true)} />
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<Helmet>
|
||||
<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") ?? ""} />
|
||||
</Helmet>
|
||||
<div className="mx-auto mt-20 w-fit max-w-2xl rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
||||
<Link to="/">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<img
|
||||
src="/images/gradientLogo.svg"
|
||||
style={{
|
||||
height: "90px",
|
||||
width: "120px"
|
||||
}}
|
||||
alt="Infisical logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<form className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-2xl font-medium text-transparent">
|
||||
Multiple Accounts Detected
|
||||
</h1>
|
||||
<p className="text-md mb-4 text-center text-white">
|
||||
<span className="text-slate-300">Your current account is: </span>{" "}
|
||||
<b>{duplicateAccounts?.data?.myAccount?.username}</b>.
|
||||
</p>
|
||||
<Alert variant="warning" hideTitle>
|
||||
<div>
|
||||
We've detected multiple accounts using variations of the same email address.
|
||||
Confirm to retain this account <b>{duplicateAccounts?.data?.myAccount?.username}</b>
|
||||
. Upon confirmation other accounts will be <b>removed</b>.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
<div className="thin-scrollbar flex h-full max-h-60 w-full flex-col items-stretch gap-2 overflow-auto rounded-md">
|
||||
{duplicateAccounts?.data?.duplicateAccounts?.map((el) => {
|
||||
const lastSession = el.devices?.at(-1);
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex items-center gap-8 rounded-md bg-mineshaft-700 px-4 py-3 text-gray-200"
|
||||
>
|
||||
<div className="group flex flex-grow flex-col">
|
||||
<div className="truncate text-sm transition-colors">{el.username}</div>
|
||||
<div className="mt-2 text-xs">
|
||||
Last login: {format(new Date(el.updatedAt), "Pp")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<div className="flex flex-col space-y-1 text-sm">
|
||||
<div>IP: {lastSession?.ip || "-"}</div>
|
||||
<div>User Agent: {lastSession?.userAgent || "-"}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 flex w-full flex-col">
|
||||
<div className="flex gap-6">
|
||||
<Button
|
||||
className="flex-1 flex-grow"
|
||||
isLoading={removeDuplicateEmails.isPending}
|
||||
onClick={() =>
|
||||
removeDuplicateEmails.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Removed duplicate accounts"
|
||||
});
|
||||
setRemoveDuplicateLater(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => setRemoveDuplicateLater(true)}
|
||||
className="flex-1 flex-grow"
|
||||
>
|
||||
Ask Again Later?
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={logout.isPending}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="mt-4"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Change Account
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="pb-28" />
|
||||
</div>
|
||||
);
|
||||
return <SelectOrganizationSection />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user