[GIT-66] improvement: prevent disabling last enabled authentication method (#8570)
This commit is contained in:
parent
f7d5200ed8
commit
32a2584578
7 changed files with 167 additions and 127 deletions
|
|
@ -1,16 +1,18 @@
|
|||
import { useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { setPromiseToast, setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { cn, resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
// helpers
|
||||
import { canDisableAuthMethod } from "@/helpers/authentication";
|
||||
// hooks
|
||||
import { useAuthenticationModes } from "@/hooks/oauth";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// types
|
||||
|
|
@ -19,48 +21,87 @@ import type { Route } from "./+types/page";
|
|||
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
|
||||
// theme
|
||||
const { resolvedTheme: resolvedThemeAdmin } = useTheme();
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
|
||||
// Ref to store authentication modes for validation (avoids circular dependency)
|
||||
const authenticationModesRef = useRef<TInstanceAuthenticationModes[]>([]);
|
||||
// state
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// derived values
|
||||
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
||||
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
|
||||
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
||||
setIsSubmitting(true);
|
||||
// Create updateConfig with validation - uses authenticationModesRef for current modes
|
||||
const updateConfig = useCallback(
|
||||
(key: TInstanceConfigurationKeys, value: string): void => {
|
||||
// Check if trying to disable (value === "0")
|
||||
if (value === "0") {
|
||||
// Check if this key is an authentication method key
|
||||
const currentAuthModes = authenticationModesRef.current;
|
||||
const isAuthMethodKey = currentAuthModes.some((method) => method.enabledConfigKey === key);
|
||||
|
||||
const payload = {
|
||||
[key]: value,
|
||||
};
|
||||
// Only validate if this is an authentication method key
|
||||
if (isAuthMethodKey) {
|
||||
const canDisable = canDisableAuthMethod(key, currentAuthModes, formattedConfig);
|
||||
|
||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
if (!canDisable) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Cannot disable authentication",
|
||||
message:
|
||||
"At least one authentication method must remain enabled. Please enable another method before disabling this one.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving configuration",
|
||||
success: {
|
||||
title: "Success",
|
||||
message: () => "Configuration saved successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
message: () => "Failed to save configuration",
|
||||
},
|
||||
});
|
||||
// Proceed with the update
|
||||
setIsSubmitting(true);
|
||||
|
||||
await updateConfigPromise
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setIsSubmitting(false);
|
||||
const payload = {
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving configuration",
|
||||
success: {
|
||||
title: "Success",
|
||||
message: () => "Configuration saved successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
message: () => "Failed to save configuration",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const authenticationModes = useAuthenticationModes({ disabled: isSubmitting, updateConfig, resolvedTheme });
|
||||
void updateConfigPromise
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
return undefined;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
},
|
||||
[formattedConfig, updateInstanceConfigurations]
|
||||
);
|
||||
|
||||
// Get authentication modes - this will use updateConfig which includes validation
|
||||
const authenticationModes = useAuthenticationModes({
|
||||
disabled: isSubmitting,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
});
|
||||
|
||||
// Update ref with latest authentication modes
|
||||
authenticationModesRef.current = authenticationModes;
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,7 @@
|
|||
import Link from "next/link";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// plane packages
|
||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
|
||||
import type { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
||||
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
||||
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
|
||||
// images
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
|
|
@ -106,53 +92,3 @@ export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string
|
|||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
{
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description:
|
||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
|
||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
name: "Google",
|
||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
||||
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
||||
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue