mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 10:31:10 +10:00
feat(EE): MFA implementation (#1381)
* feat(EE): MFA implementation for enterprise edition - Add TOTP-based two-factor authentication - Add backup codes support - Add MFA enforcement at workspace level - Add MFA setup and challenge UI pages - Support MFA for login and password reset flows - Add MFA validation for secure pages * fix types * remove unused object * sync * remove unused type * sync * refactor: rename MFA enabled field to is_enabled * sync
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as z from "zod";
|
||||
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
@ -11,6 +11,7 @@ import {
|
||||
Box,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import { zodResolver } from "mantine-form-zod-resolver";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { IRegister } from "@/features/auth/types/auth.types";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
|
||||
@ -27,7 +27,7 @@ import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { RESET } from "jotai/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { exchangeTokenRedirectUrl, getHostnameUrl } from "@/ee/utils.ts";
|
||||
import { exchangeTokenRedirectUrl } from "@/ee/utils.ts";
|
||||
|
||||
export default function useAuth() {
|
||||
const { t } = useTranslation();
|
||||
@ -39,9 +39,17 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await login(data);
|
||||
const response = await login(data);
|
||||
setIsLoading(false);
|
||||
navigate(APP_ROUTE.HOME);
|
||||
|
||||
// Check if MFA is required
|
||||
if (response?.userHasMfa) {
|
||||
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE);
|
||||
} else if (response?.requiresMfaSetup) {
|
||||
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
|
||||
} else {
|
||||
navigate(APP_ROUTE.HOME);
|
||||
}
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
console.log(err);
|
||||
@ -56,9 +64,19 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await acceptInvitation(data);
|
||||
const response = await acceptInvitation(data);
|
||||
setIsLoading(false);
|
||||
navigate(APP_ROUTE.HOME);
|
||||
|
||||
if (response?.requiresLogin) {
|
||||
notifications.show({
|
||||
message: t(
|
||||
"Account created successfully. Please log in to set up two-factor authentication.",
|
||||
),
|
||||
});
|
||||
navigate(APP_ROUTE.AUTH.LOGIN);
|
||||
} else {
|
||||
navigate(APP_ROUTE.HOME);
|
||||
}
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
notifications.show({
|
||||
@ -100,12 +118,22 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await passwordReset(data);
|
||||
const response = await passwordReset(data);
|
||||
setIsLoading(false);
|
||||
navigate(APP_ROUTE.HOME);
|
||||
notifications.show({
|
||||
message: t("Password reset was successful"),
|
||||
});
|
||||
|
||||
if (response?.requiresLogin) {
|
||||
notifications.show({
|
||||
message: t(
|
||||
"Password reset was successful. Please log in with your new password.",
|
||||
),
|
||||
});
|
||||
navigate(APP_ROUTE.AUTH.LOGIN);
|
||||
} else {
|
||||
navigate(APP_ROUTE.HOME);
|
||||
notifications.show({
|
||||
message: t("Password reset was successful"),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
notifications.show({
|
||||
|
||||
@ -4,14 +4,16 @@ import {
|
||||
ICollabToken,
|
||||
IForgotPassword,
|
||||
ILogin,
|
||||
ILoginResponse,
|
||||
IPasswordReset,
|
||||
ISetupWorkspace,
|
||||
IVerifyUserToken,
|
||||
} from "@/features/auth/types/auth.types";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
|
||||
export async function login(data: ILogin): Promise<void> {
|
||||
await api.post<void>("/auth/login", data);
|
||||
export async function login(data: ILogin): Promise<ILoginResponse> {
|
||||
const response = await api.post<ILoginResponse>("/auth/login", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
@ -36,8 +38,9 @@ export async function forgotPassword(data: IForgotPassword): Promise<void> {
|
||||
await api.post<void>("/auth/forgot-password", data);
|
||||
}
|
||||
|
||||
export async function passwordReset(data: IPasswordReset): Promise<void> {
|
||||
await api.post<void>("/auth/password-reset", data);
|
||||
export async function passwordReset(data: IPasswordReset): Promise<{ requiresLogin?: boolean; }> {
|
||||
const req = await api.post("/auth/password-reset", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function verifyUserToken(data: IVerifyUserToken): Promise<any> {
|
||||
@ -47,4 +50,4 @@ export async function verifyUserToken(data: IVerifyUserToken): Promise<any> {
|
||||
export async function getCollabToken(): Promise<ICollabToken> {
|
||||
const req = await api.post<ICollabToken>("/auth/collab-token");
|
||||
return req.data;
|
||||
}
|
||||
}
|
||||
@ -38,3 +38,10 @@ export interface IVerifyUserToken {
|
||||
export interface ICollabToken {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface ILoginResponse {
|
||||
userHasMfa?: boolean;
|
||||
requiresMfaSetup?: boolean;
|
||||
mfaToken?: string;
|
||||
isMfaEnforced?: boolean;
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { isCloud } from "@/lib/config";
|
||||
import { useLicense } from "@/ee/hooks/use-license";
|
||||
import { MfaSettings } from "@/ee/mfa";
|
||||
|
||||
export function AccountMfaSection() {
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const showMfa = isCloud() || hasLicenseKey;
|
||||
|
||||
if (!showMfa) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MfaSettings />;
|
||||
}
|
||||
@ -22,7 +22,7 @@ export default function ChangeEmail() {
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="md">{t("Email")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{currentUser?.user.email}
|
||||
@ -30,7 +30,7 @@ export default function ChangeEmail() {
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Button onClick={open} variant="default">
|
||||
<Button onClick={open} variant="default" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("Change email")}
|
||||
</Button>
|
||||
*/}
|
||||
|
||||
@ -14,14 +14,14 @@ export default function ChangePassword() {
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="md">{t("Password")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("You can change your password here.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button onClick={open} variant="default">
|
||||
<Button onClick={open} variant="default" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("Change password")}
|
||||
</Button>
|
||||
|
||||
|
||||
@ -66,8 +66,9 @@ export async function createInvitation(data: ICreateInvite) {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function acceptInvitation(data: IAcceptInvite): Promise<void> {
|
||||
await api.post<void>("/workspace/invites/accept", data);
|
||||
export async function acceptInvitation(data: IAcceptInvite): Promise<{ requiresLogin?: boolean; }> {
|
||||
const req = await api.post("/workspace/invites/accept", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getInviteLink(data: {
|
||||
|
||||
@ -21,6 +21,7 @@ export interface IWorkspace {
|
||||
memberCount?: number;
|
||||
plan?: string;
|
||||
hasLicenseKey?: boolean;
|
||||
enforceMfa?: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateInvite {
|
||||
|
||||
Reference in New Issue
Block a user