mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-21 14:49:07 +00:00
feat: Add VERIFY_TWO_FACTOR_AUTH_API_URL constant
This commit adds the VERIFY_TWO_FACTOR_AUTH_API_URL constant to the ApiPaths module in order to provide a URL for verifying two-factor authentication. This constant is used in the Authentication module to make API requests for verifying the user's two-factor authentication. The constant is constructed using the IDENTITY_URL and a new route "/verify-two-factor-auth". This addition enables the implementation of the two-factor authentication verification feature. Files modified: - Accounts/src/Utils/ApiPaths.ts
This commit is contained in:
parent
0a5094db37
commit
05a26d0b3f
@ -1,7 +1,10 @@
|
|||||||
import { LOGIN_API_URL } from "../Utils/ApiPaths";
|
import {
|
||||||
|
LOGIN_API_URL,
|
||||||
|
VERIFY_TWO_FACTOR_AUTH_API_URL,
|
||||||
|
} from "../Utils/ApiPaths";
|
||||||
import Route from "Common/Types/API/Route";
|
import Route from "Common/Types/API/Route";
|
||||||
import URL from "Common/Types/API/URL";
|
import URL from "Common/Types/API/URL";
|
||||||
import { JSONObject } from "Common/Types/JSON";
|
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||||
import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
|
import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
|
||||||
import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
|
import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
|
||||||
import Link from "CommonUI/src/Components/Link/Link";
|
import Link from "CommonUI/src/Components/Link/Link";
|
||||||
@ -9,11 +12,17 @@ import { DASHBOARD_URL } from "CommonUI/src/Config";
|
|||||||
import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
|
import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
|
||||||
import UiAnalytics from "CommonUI/src/Utils/Analytics";
|
import UiAnalytics from "CommonUI/src/Utils/Analytics";
|
||||||
import LoginUtil from "CommonUI/src/Utils/Login";
|
import LoginUtil from "CommonUI/src/Utils/Login";
|
||||||
|
import UserTwoFactorAuth from "Model/Models/UserTwoFactorAuth";
|
||||||
import Navigation from "CommonUI/src/Utils/Navigation";
|
import Navigation from "CommonUI/src/Utils/Navigation";
|
||||||
import UserUtil from "CommonUI/src/Utils/User";
|
import UserUtil from "CommonUI/src/Utils/User";
|
||||||
import User from "Model/Models/User";
|
import User from "Model/Models/User";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import useAsyncEffect from "use-async-effect";
|
import useAsyncEffect from "use-async-effect";
|
||||||
|
import StaticModelList from "CommonUI/src/Components/ModelList/StaticModelList";
|
||||||
|
import BasicForm from "CommonUI/src/Components/Forms/BasicForm";
|
||||||
|
import API from "CommonUI/src/Utils/API/API";
|
||||||
|
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||||
|
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||||
|
|
||||||
const LoginPage: () => JSX.Element = () => {
|
const LoginPage: () => JSX.Element = () => {
|
||||||
const apiUrl: URL = LOGIN_API_URL;
|
const apiUrl: URL = LOGIN_API_URL;
|
||||||
@ -24,6 +33,22 @@ const LoginPage: () => JSX.Element = () => {
|
|||||||
|
|
||||||
const [initialValues, setInitialValues] = React.useState<JSONObject>({});
|
const [initialValues, setInitialValues] = React.useState<JSONObject>({});
|
||||||
|
|
||||||
|
const [showTwoFactorAuth, setShowTwoFactorAuth] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
|
||||||
|
const [twoFactorAuthList, setTwoFactorAuthList] = React.useState<
|
||||||
|
UserTwoFactorAuth[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] = React.useState<
|
||||||
|
UserTwoFactorAuth | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const [isTwoFactorAuthLoading, setIsTwoFactorAuthLoading] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
const [twofactorAuthError, setTwoFactorAuthError] =
|
||||||
|
React.useState<string>("");
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
if (Navigation.getQueryStringByName("email")) {
|
if (Navigation.getQueryStringByName("email")) {
|
||||||
setInitialValues({
|
setInitialValues({
|
||||||
@ -32,6 +57,20 @@ const LoginPage: () => JSX.Element = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
type LoginFunction = (user: User, miscData: JSONObject) => void;
|
||||||
|
|
||||||
|
const login: LoginFunction = (user: User, miscData: JSONObject): void => {
|
||||||
|
if (user instanceof User && user && user.email) {
|
||||||
|
UiAnalytics.userAuth(user.email);
|
||||||
|
UiAnalytics.capture("accounts/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginUtil.login({
|
||||||
|
user: user,
|
||||||
|
token: miscData ? miscData["token"] : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div className="">
|
<div className="">
|
||||||
@ -51,67 +90,145 @@ const LoginPage: () => JSX.Element = () => {
|
|||||||
|
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||||
<ModelForm<User>
|
{!showTwoFactorAuth && (
|
||||||
modelType={User}
|
<ModelForm<User>
|
||||||
id="login-form"
|
modelType={User}
|
||||||
name="Login"
|
id="login-form"
|
||||||
fields={[
|
name="Login"
|
||||||
{
|
fields={[
|
||||||
field: {
|
{
|
||||||
email: true,
|
field: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
fieldType: FormFieldSchemaType.Email,
|
||||||
|
placeholder: "jeff@example.com",
|
||||||
|
required: true,
|
||||||
|
disabled: Boolean(initialValues && initialValues["email"]),
|
||||||
|
title: "Email",
|
||||||
|
dataTestId: "email",
|
||||||
},
|
},
|
||||||
fieldType: FormFieldSchemaType.Email,
|
{
|
||||||
placeholder: "jeff@example.com",
|
field: {
|
||||||
required: true,
|
password: true,
|
||||||
disabled: Boolean(initialValues && initialValues["email"]),
|
},
|
||||||
title: "Email",
|
title: "Password",
|
||||||
dataTestId: "email",
|
required: true,
|
||||||
},
|
validation: {
|
||||||
{
|
minLength: 6,
|
||||||
field: {
|
},
|
||||||
password: true,
|
fieldType: FormFieldSchemaType.Password,
|
||||||
|
sideLink: {
|
||||||
|
text: "Forgot password?",
|
||||||
|
url: new Route("/accounts/forgot-password"),
|
||||||
|
openLinkInNewTab: false,
|
||||||
|
},
|
||||||
|
dataTestId: "password",
|
||||||
},
|
},
|
||||||
title: "Password",
|
]}
|
||||||
required: true,
|
createOrUpdateApiUrl={apiUrl}
|
||||||
validation: {
|
formType={FormType.Create}
|
||||||
minLength: 6,
|
submitButtonText={"Login"}
|
||||||
},
|
onBeforeCreate={(data: User) => {
|
||||||
fieldType: FormFieldSchemaType.Password,
|
setInitialValues(User.toJSON(data, User));
|
||||||
sideLink: {
|
return Promise.resolve(data);
|
||||||
text: "Forgot password?",
|
}}
|
||||||
url: new Route("/accounts/forgot-password"),
|
onSuccess={(
|
||||||
openLinkInNewTab: false,
|
value: User | JSONObject,
|
||||||
},
|
miscData: JSONObject | undefined,
|
||||||
dataTestId: "password",
|
) => {
|
||||||
},
|
if ((value as JSONObject)["twoFactorAuth"] === true) {
|
||||||
]}
|
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||||
createOrUpdateApiUrl={apiUrl}
|
UserTwoFactorAuth.fromJSONArray(
|
||||||
formType={FormType.Create}
|
(value as JSONObject)["twoFactorAuthList"] as JSONArray,
|
||||||
submitButtonText={"Login"}
|
UserTwoFactorAuth,
|
||||||
onSuccess={(value: User, miscData: JSONObject | undefined) => {
|
);
|
||||||
if (value && value.email) {
|
setTwoFactorAuthList(twoFactorAuthList);
|
||||||
UiAnalytics.userAuth(value.email);
|
setShowTwoFactorAuth(true);
|
||||||
UiAnalytics.capture("accounts/login");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginUtil.login({
|
login(value as User, miscData as JSONObject);
|
||||||
user: value,
|
}}
|
||||||
token: miscData ? miscData["token"] : undefined,
|
maxPrimaryButtonWidth={true}
|
||||||
});
|
footer={
|
||||||
}}
|
<div className="actions text-center mt-4 hover:underline fw-semibold">
|
||||||
maxPrimaryButtonWidth={true}
|
<div>
|
||||||
footer={
|
<Link to={new Route("/accounts/sso")}>
|
||||||
<div className="actions text-center mt-4 hover:underline fw-semibold">
|
<div className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm">
|
||||||
<div>
|
Use single sign-on (SSO) instead
|
||||||
<Link to={new Route("/accounts/sso")}>
|
</div>
|
||||||
<div className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm">
|
</Link>
|
||||||
Use single sign-on (SSO) instead
|
</div>
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
)}
|
||||||
|
|
||||||
|
{showTwoFactorAuth && !selectedTwoFactorAuth && (
|
||||||
|
<StaticModelList<UserTwoFactorAuth>
|
||||||
|
titleField="name"
|
||||||
|
descriptionField=""
|
||||||
|
selectedItems={[]}
|
||||||
|
list={twoFactorAuthList}
|
||||||
|
onClick={(item: UserTwoFactorAuth) => {
|
||||||
|
setSelectedTwoFactorAuth(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showTwoFactorAuth && selectedTwoFactorAuth && (
|
||||||
|
<BasicForm
|
||||||
|
id="two-factor-auth-form"
|
||||||
|
name="Two Factor Auth"
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
field: {
|
||||||
|
code: true,
|
||||||
|
},
|
||||||
|
title: "Code",
|
||||||
|
description: "Enter the code from your authenticator app",
|
||||||
|
required: true,
|
||||||
|
dataTestId: "code",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
isLoading={isTwoFactorAuthLoading}
|
||||||
|
error={twofactorAuthError}
|
||||||
|
submitButtonText={"Submit"}
|
||||||
|
onSubmit={async (data: JSONObject) => {
|
||||||
|
setIsTwoFactorAuthLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const code: string = data["code"] as string;
|
||||||
|
|
||||||
|
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||||
|
await API.post(VERIFY_TWO_FACTOR_AUTH_API_URL, {
|
||||||
|
data: initialValues,
|
||||||
|
code: code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result instanceof HTTPErrorResponse) {
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: User = User.fromJSON(
|
||||||
|
result["data"] as JSONObject,
|
||||||
|
User,
|
||||||
|
) as User;
|
||||||
|
const miscData: JSONObject = (result["data"] as JSONObject)[
|
||||||
|
"miscData"
|
||||||
|
] as JSONObject;
|
||||||
|
|
||||||
|
login(user as User, miscData as JSONObject);
|
||||||
|
} catch (error) {
|
||||||
|
setTwoFactorAuthError(
|
||||||
|
API.getFriendlyErrorMessage(error as Error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTwoFactorAuthLoading(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 text-center">
|
<div className="mt-10 text-center">
|
||||||
<div className="text-muted mb-0 text-gray-500">
|
<div className="text-muted mb-0 text-gray-500">
|
||||||
|
@ -9,6 +9,10 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
|||||||
new Route("/login"),
|
new Route("/login"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const VERIFY_TWO_FACTOR_AUTH_API_URL: URL = URL.fromURL(
|
||||||
|
IDENTITY_URL,
|
||||||
|
).addRoute(new Route("/verify-two-factor-auth"));
|
||||||
|
|
||||||
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(
|
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(
|
||||||
IDENTITY_URL,
|
IDENTITY_URL,
|
||||||
).addRoute(new Route("/service-provider-login"));
|
).addRoute(new Route("/service-provider-login"));
|
||||||
|
@ -518,59 +518,6 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/fetch-two-factor-auth-list",
|
|
||||||
async (
|
|
||||||
req: ExpressRequest,
|
|
||||||
res: ExpressResponse,
|
|
||||||
next: NextFunction,
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const data: JSONObject = req.body["data"];
|
|
||||||
|
|
||||||
if (!data["userId"]) {
|
|
||||||
return Response.sendErrorResponse(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
new BadDataException(
|
|
||||||
"User Id is required to fetch the two factor auth list.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId: ObjectID = new ObjectID(data["userId"] as string);
|
|
||||||
|
|
||||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
|
||||||
await UserTwoFactorAuthService.findBy({
|
|
||||||
query: {
|
|
||||||
userId: userId,
|
|
||||||
isVerified: true,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
_id: true,
|
|
||||||
userId: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
limit: LIMIT_PER_PROJECT,
|
|
||||||
skip: 0,
|
|
||||||
props: {
|
|
||||||
isRoot: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.sendEntityArrayResponse(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
twoFactorAuthList,
|
|
||||||
twoFactorAuthList.length,
|
|
||||||
UserTwoFactorAuth,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/login",
|
"/login",
|
||||||
async (
|
async (
|
||||||
@ -587,7 +534,42 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const login = async (options: {
|
type FetchTwoFactorAuthListFunction = (
|
||||||
|
userId: ObjectID,
|
||||||
|
) => Promise<Array<UserTwoFactorAuth>>;
|
||||||
|
|
||||||
|
const fetchTwoFactorAuthList: FetchTwoFactorAuthListFunction = async (
|
||||||
|
userId: ObjectID,
|
||||||
|
): Promise<Array<UserTwoFactorAuth>> => {
|
||||||
|
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||||
|
await UserTwoFactorAuthService.findBy({
|
||||||
|
query: {
|
||||||
|
userId: userId,
|
||||||
|
isVerified: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
_id: true,
|
||||||
|
userId: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
limit: LIMIT_PER_PROJECT,
|
||||||
|
skip: 0,
|
||||||
|
props: {
|
||||||
|
isRoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return twoFactorAuthList;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginFunction = (options: {
|
||||||
|
req: ExpressRequest;
|
||||||
|
res: ExpressResponse;
|
||||||
|
next: NextFunction;
|
||||||
|
verifyTwoFactorAuth: boolean;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
const login: LoginFunction = async (options: {
|
||||||
req: ExpressRequest;
|
req: ExpressRequest;
|
||||||
res: ExpressResponse;
|
res: ExpressResponse;
|
||||||
next: NextFunction;
|
next: NextFunction;
|
||||||
@ -648,8 +630,25 @@ const login = async (options: {
|
|||||||
|
|
||||||
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
|
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
|
||||||
// If two factor auth is enabled then we will send the user to the two factor auth page.
|
// If two factor auth is enabled then we will send the user to the two factor auth page.
|
||||||
|
|
||||||
|
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||||
|
await fetchTwoFactorAuthList(alreadySavedUser.id!);
|
||||||
|
|
||||||
|
if (!twoFactorAuthList || twoFactorAuthList.length === 0) {
|
||||||
|
const errorMessage: string = IsBillingEnabled
|
||||||
|
? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help."
|
||||||
|
: "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account.";
|
||||||
|
|
||||||
|
return Response.sendErrorResponse(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
new BadDataException(errorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Response.sendJsonObjectResponse(req, res, {
|
return Response.sendJsonObjectResponse(req, res, {
|
||||||
twoFactorAuth: true,
|
twoFactorAuth: true,
|
||||||
|
twoFactorAuthList: twoFactorAuthList,
|
||||||
userId: alreadySavedUser.id?.toString(),
|
userId: alreadySavedUser.id?.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user