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:
Simon Larsen 2024-07-28 10:52:11 -06:00
parent 0a5094db37
commit 05a26d0b3f
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
3 changed files with 233 additions and 113 deletions

View File

@ -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">

View File

@ -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"));

View File

@ -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(),
}); });
} }