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) {
|
if (duplicatedAccounts.length && myAccount) {
|
||||||
await userDAL.transaction(async (tx) => {
|
await userDAL.transaction(async (tx) => {
|
||||||
await userDAL.delete({ $in: { id: duplicatedAccounts?.map((el) => el.id) } }, 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
|
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 = () =>
|
export const useGetMyDuplicateAccount = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: userKeys.getMyDuplicateAccount,
|
queryKey: userKeys.getMyDuplicateAccount,
|
||||||
queryFn: async () => {
|
staleTime: 60 * 1000, // 1 min in ms
|
||||||
const { data } = await apiRequest.get<{
|
queryFn: fetchUserDuplicateAccounts,
|
||||||
users: Array<User & { isMyAccount: boolean; devices: { ip: string; userAgent: string }[] }>;
|
|
||||||
}>("/api/v1/user/duplicate-accounts");
|
|
||||||
return data.users;
|
|
||||||
},
|
|
||||||
select: (users) => ({
|
select: (users) => ({
|
||||||
duplicateAccounts: users.filter((el) => !el.isMyAccount),
|
duplicateAccounts: users.filter((el) => !el.isMyAccount),
|
||||||
myAccount: users?.find((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 { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
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";
|
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export const PasswordStep = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(true);
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
@@ -109,6 +111,13 @@ export const PasswordStep = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userDuplicateAccount = await fetchUserDuplicateAccounts();
|
||||||
|
const hasDuplicate = userDuplicateAccount?.length > 1;
|
||||||
|
if (hasDuplicate) {
|
||||||
|
setRemoveDuplicateLater(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await navigateUserToOrg(navigate, organizationId);
|
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) {
|
if (hasExchangedPrivateKey) {
|
||||||
return (
|
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">
|
<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 { 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 { createNotification } from "@app/components/notifications";
|
import { Spinner } from "@app/components/v2";
|
||||||
import { Alert, Button, Spinner, Tooltip } from "@app/components/v2";
|
import { useGetMyDuplicateAccount } from "@app/hooks/api";
|
||||||
import {
|
|
||||||
useGetMyDuplicateAccount,
|
|
||||||
useLogoutUser,
|
|
||||||
useRemoveMyDuplicateAccounts
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
|
import { EmailDuplicationConfirmation } from "./EmailDuplicationConfirmation";
|
||||||
import { SelectOrganizationSection } from "./SelectOrgSection";
|
import { SelectOrganizationSection } from "./SelectOrgSection";
|
||||||
|
|
||||||
const LoadingScreen = () => {
|
const LoadingScreen = () => {
|
||||||
@@ -27,139 +17,17 @@ const LoadingScreen = () => {
|
|||||||
|
|
||||||
export const SelectOrganizationPage = () => {
|
export const SelectOrganizationPage = () => {
|
||||||
const duplicateAccounts = useGetMyDuplicateAccount();
|
const duplicateAccounts = useGetMyDuplicateAccount();
|
||||||
const removeDuplicateEmails = useRemoveMyDuplicateAccounts();
|
|
||||||
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(false);
|
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) {
|
if (duplicateAccounts.isPending) {
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!duplicateAccounts.data?.duplicateAccounts?.length || removeDuplicateLater) {
|
if (duplicateAccounts.data?.duplicateAccounts?.length && !removeDuplicateLater) {
|
||||||
return <SelectOrganizationSection />;
|
return (
|
||||||
|
<EmailDuplicationConfirmation onRemoveDuplicateLater={() => setRemoveDuplicateLater(true)} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <SelectOrganizationSection />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user