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 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 FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
|
||||
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 UiAnalytics from "CommonUI/src/Utils/Analytics";
|
||||
import LoginUtil from "CommonUI/src/Utils/Login";
|
||||
import UserTwoFactorAuth from "Model/Models/UserTwoFactorAuth";
|
||||
import Navigation from "CommonUI/src/Utils/Navigation";
|
||||
import UserUtil from "CommonUI/src/Utils/User";
|
||||
import User from "Model/Models/User";
|
||||
import React from "react";
|
||||
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 apiUrl: URL = LOGIN_API_URL;
|
||||
@ -24,6 +33,22 @@ const LoginPage: () => JSX.Element = () => {
|
||||
|
||||
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 () => {
|
||||
if (Navigation.getQueryStringByName("email")) {
|
||||
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 (
|
||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<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="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<ModelForm<User>
|
||||
modelType={User}
|
||||
id="login-form"
|
||||
name="Login"
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
email: true,
|
||||
{!showTwoFactorAuth && (
|
||||
<ModelForm<User>
|
||||
modelType={User}
|
||||
id="login-form"
|
||||
name="Login"
|
||||
fields={[
|
||||
{
|
||||
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",
|
||||
required: true,
|
||||
disabled: Boolean(initialValues && initialValues["email"]),
|
||||
title: "Email",
|
||||
dataTestId: "email",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
password: true,
|
||||
{
|
||||
field: {
|
||||
password: true,
|
||||
},
|
||||
title: "Password",
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 6,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
sideLink: {
|
||||
text: "Forgot password?",
|
||||
url: new Route("/accounts/forgot-password"),
|
||||
openLinkInNewTab: false,
|
||||
},
|
||||
dataTestId: "password",
|
||||
},
|
||||
title: "Password",
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 6,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
sideLink: {
|
||||
text: "Forgot password?",
|
||||
url: new Route("/accounts/forgot-password"),
|
||||
openLinkInNewTab: false,
|
||||
},
|
||||
dataTestId: "password",
|
||||
},
|
||||
]}
|
||||
createOrUpdateApiUrl={apiUrl}
|
||||
formType={FormType.Create}
|
||||
submitButtonText={"Login"}
|
||||
onSuccess={(value: User, miscData: JSONObject | undefined) => {
|
||||
if (value && value.email) {
|
||||
UiAnalytics.userAuth(value.email);
|
||||
UiAnalytics.capture("accounts/login");
|
||||
}
|
||||
]}
|
||||
createOrUpdateApiUrl={apiUrl}
|
||||
formType={FormType.Create}
|
||||
submitButtonText={"Login"}
|
||||
onBeforeCreate={(data: User) => {
|
||||
setInitialValues(User.toJSON(data, User));
|
||||
return Promise.resolve(data);
|
||||
}}
|
||||
onSuccess={(
|
||||
value: User | JSONObject,
|
||||
miscData: JSONObject | undefined,
|
||||
) => {
|
||||
if ((value as JSONObject)["twoFactorAuth"] === true) {
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
UserTwoFactorAuth.fromJSONArray(
|
||||
(value as JSONObject)["twoFactorAuthList"] as JSONArray,
|
||||
UserTwoFactorAuth,
|
||||
);
|
||||
setTwoFactorAuthList(twoFactorAuthList);
|
||||
setShowTwoFactorAuth(true);
|
||||
return;
|
||||
}
|
||||
|
||||
LoginUtil.login({
|
||||
user: value,
|
||||
token: miscData ? miscData["token"] : undefined,
|
||||
});
|
||||
}}
|
||||
maxPrimaryButtonWidth={true}
|
||||
footer={
|
||||
<div className="actions text-center mt-4 hover:underline fw-semibold">
|
||||
<div>
|
||||
<Link to={new Route("/accounts/sso")}>
|
||||
<div className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm">
|
||||
Use single sign-on (SSO) instead
|
||||
</div>
|
||||
</Link>
|
||||
login(value as User, miscData as JSONObject);
|
||||
}}
|
||||
maxPrimaryButtonWidth={true}
|
||||
footer={
|
||||
<div className="actions text-center mt-4 hover:underline fw-semibold">
|
||||
<div>
|
||||
<Link to={new Route("/accounts/sso")}>
|
||||
<div className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm">
|
||||
Use single sign-on (SSO) instead
|
||||
</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 className="mt-10 text-center">
|
||||
<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"),
|
||||
);
|
||||
|
||||
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(
|
||||
IDENTITY_URL,
|
||||
).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(
|
||||
"/login",
|
||||
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;
|
||||
res: ExpressResponse;
|
||||
next: NextFunction;
|
||||
@ -648,8 +630,25 @@ const login = async (options: {
|
||||
|
||||
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
|
||||
// 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, {
|
||||
twoFactorAuth: true,
|
||||
twoFactorAuthList: twoFactorAuthList,
|
||||
userId: alreadySavedUser.id?.toString(),
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user