mirror of
https://github.com/usememos/memos.git
synced 2025-02-19 04:40:40 +01:00
feat: add system setting to disable password-based login (#2039)
* system setting to disable password login * fix linter warning * fix indentation warning * Prohibit disable-password-login if no identity providers are configured * Warnings and explicit confirmation when en-/disabling password-login - Disabling password login now gives a warning and requires a second confirmation which needs to be explicitly typed. - (Re)Enabling password login now also gives a simple warning. - Removing an identity provider while password-login is disabled now also warns about possible problems. * Fix formatting * Fix code-style --------- Co-authored-by: traumweh <5042134-traumweh@users.noreply.gitlab.com>
This commit is contained in:
parent
9ef0f8a901
commit
c1cbfd5766
@ -37,6 +37,24 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
|
||||
g.POST("/auth/signin", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
signin := &SignIn{}
|
||||
|
||||
disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||
Name: SystemSettingDisablePasswordLoginName.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
|
||||
}
|
||||
if disablePasswordLoginSystemSetting != nil {
|
||||
disablePasswordLogin := false
|
||||
err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||
}
|
||||
if disablePasswordLogin {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ type SystemStatus struct {
|
||||
// System settings
|
||||
// Allow sign up.
|
||||
AllowSignUp bool `json:"allowSignUp"`
|
||||
// Disable password login.
|
||||
DisablePasswordLogin bool `json:"disablePasswordLogin"`
|
||||
// Disable public memos.
|
||||
DisablePublicMemos bool `json:"disablePublicMemos"`
|
||||
// Max upload size.
|
||||
@ -48,14 +50,15 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
systemStatus := SystemStatus{
|
||||
Profile: *s.Profile,
|
||||
DBSize: 0,
|
||||
AllowSignUp: false,
|
||||
DisablePublicMemos: false,
|
||||
MaxUploadSizeMiB: 32,
|
||||
AutoBackupInterval: 0,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
Profile: *s.Profile,
|
||||
DBSize: 0,
|
||||
AllowSignUp: false,
|
||||
DisablePasswordLogin: false,
|
||||
DisablePublicMemos: false,
|
||||
MaxUploadSizeMiB: 32,
|
||||
AutoBackupInterval: 0,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
CustomizedProfile: CustomizedProfile{
|
||||
Name: "memos",
|
||||
LogoURL: "",
|
||||
@ -103,6 +106,8 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
|
||||
switch systemSetting.Name {
|
||||
case SystemSettingAllowSignUpName.String():
|
||||
systemStatus.AllowSignUp = baseValue.(bool)
|
||||
case SystemSettingDisablePasswordLoginName.String():
|
||||
systemStatus.DisablePasswordLogin = baseValue.(bool)
|
||||
case SystemSettingDisablePublicMemosName.String():
|
||||
systemStatus.DisablePublicMemos = baseValue.(bool)
|
||||
case SystemSettingMaxUploadSizeMiBName.String():
|
||||
|
@ -19,6 +19,8 @@ const (
|
||||
SystemSettingSecretSessionName SystemSettingName = "secret-session"
|
||||
// SystemSettingAllowSignUpName is the name of allow signup setting.
|
||||
SystemSettingAllowSignUpName SystemSettingName = "allow-signup"
|
||||
// SystemSettingDisablePasswordLoginName is the name of disable password login setting.
|
||||
SystemSettingDisablePasswordLoginName SystemSettingName = "disable-password-login"
|
||||
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
|
||||
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
|
||||
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
|
||||
@ -92,6 +94,11 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
case SystemSettingDisablePasswordLoginName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
case SystemSettingDisablePublicMemosName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
@ -201,6 +208,20 @@ func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
|
||||
if err := systemSettingUpsert.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
|
||||
}
|
||||
if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName {
|
||||
var disablePasswordLogin bool
|
||||
if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
|
||||
}
|
||||
|
||||
identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
|
||||
}
|
||||
if disablePasswordLogin && len(identityProviderList) == 0 {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.")
|
||||
}
|
||||
}
|
||||
|
||||
systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
|
||||
Name: systemSettingUpsert.Name.String(),
|
||||
|
98
web/src/components/DisablePasswordLoginDialog.tsx
Normal file
98
web/src/components/DisablePasswordLoginDialog.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Button } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { useGlobalStore } from "@/store/module";
|
||||
import * as api from "@/helpers/api";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
type Props = DialogProps;
|
||||
|
||||
interface State {
|
||||
disablePasswordLogin: boolean;
|
||||
}
|
||||
|
||||
const DisablePasswordLoginDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const t = useTranslate();
|
||||
const globalStore = useGlobalStore();
|
||||
const systemStatus = globalStore.state.systemStatus;
|
||||
const [state, setState] = useState<State>({
|
||||
disablePasswordLogin: systemStatus.disablePasswordLogin,
|
||||
});
|
||||
const [confirmedOnce, setConfirmedOnce] = useState(false);
|
||||
const [typingConfirmation, setTypingConfirmation] = useState("");
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const allowConfirmAction = () => {
|
||||
return !confirmedOnce || typingConfirmation === "CONFIRM";
|
||||
};
|
||||
|
||||
const handleConfirmBtnClick = async () => {
|
||||
if (!confirmedOnce) {
|
||||
setConfirmedOnce(true);
|
||||
} else {
|
||||
setState({ ...state, disablePasswordLogin: true });
|
||||
globalStore.setSystemStatus({ disablePasswordLogin: true });
|
||||
try {
|
||||
await api.upsertSystemSetting({
|
||||
name: "disable-password-login",
|
||||
value: JSON.stringify(true),
|
||||
});
|
||||
handleCloseBtnClick();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.response.data.message || t("message.updating-setting-failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypingConfirmationChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
setTypingConfirmation(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container !w-64">
|
||||
<p className="title-text">{t("setting.system-section.disable-password-login")}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container !w-64">
|
||||
{confirmedOnce ? (
|
||||
<>
|
||||
<p className="content-text">{t("setting.system-section.disable-password-login-final-warning")}</p>
|
||||
<input type="text" className="input-text" value={typingConfirmation} onChange={handleTypingConfirmationChanged} />
|
||||
</>
|
||||
) : (
|
||||
<p className="content-text">{t("setting.system-section.disable-password-login-warning")}</p>
|
||||
)}
|
||||
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
<Button onClick={handleConfirmBtnClick} color="danger" disabled={!allowConfirmAction()}>
|
||||
{t("common.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showDisablePasswordLoginDialog() {
|
||||
generateDialog(
|
||||
{
|
||||
className: "disable-password-login-dialog",
|
||||
dialogName: "disable-password-login-dialog",
|
||||
},
|
||||
DisablePasswordLoginDialog
|
||||
);
|
||||
}
|
||||
|
||||
export default showDisablePasswordLoginDialog;
|
@ -7,9 +7,19 @@ import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
|
||||
import Dropdown from "../kit/Dropdown";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import LearnMore from "../LearnMore";
|
||||
import { useGlobalStore } from "@/store/module";
|
||||
|
||||
interface State {
|
||||
disablePasswordLogin: boolean;
|
||||
}
|
||||
|
||||
const SSOSection = () => {
|
||||
const t = useTranslate();
|
||||
const globalStore = useGlobalStore();
|
||||
const systemStatus = globalStore.state.systemStatus;
|
||||
const [state] = useState<State>({
|
||||
disablePasswordLogin: systemStatus.disablePasswordLogin,
|
||||
});
|
||||
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -22,9 +32,15 @@ const SSOSection = () => {
|
||||
};
|
||||
|
||||
const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => {
|
||||
let content = t("setting.sso-section.confirm-delete", { name: identityProvider.name });
|
||||
|
||||
if (state.disablePasswordLogin) {
|
||||
content += "\n\n" + t("setting.sso-section.disabled-password-login-warning");
|
||||
}
|
||||
|
||||
showCommonDialog({
|
||||
title: t("setting.sso-section.delete-sso"),
|
||||
content: t("setting.sso-section.confirm-delete", { name: identityProvider.name }),
|
||||
content: content,
|
||||
style: "warning",
|
||||
dialogName: "delete-identity-provider-dialog",
|
||||
onConfirm: async () => {
|
||||
|
@ -9,10 +9,13 @@ import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"
|
||||
import Icon from "../Icon";
|
||||
import LearnMore from "../LearnMore";
|
||||
import "@/less/settings/system-section.less";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import showDisablePasswordLoginDialog from "../DisablePasswordLoginDialog";
|
||||
|
||||
interface State {
|
||||
dbSize: number;
|
||||
allowSignUp: boolean;
|
||||
disablePasswordLogin: boolean;
|
||||
disablePublicMemos: boolean;
|
||||
additionalStyle: string;
|
||||
additionalScript: string;
|
||||
@ -28,6 +31,7 @@ const SystemSection = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
dbSize: systemStatus.dbSize,
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
disablePasswordLogin: systemStatus.disablePasswordLogin,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
@ -55,6 +59,7 @@ const SystemSection = () => {
|
||||
...state,
|
||||
dbSize: systemStatus.dbSize,
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
disablePasswordLogin: systemStatus.disablePasswordLogin,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
@ -76,6 +81,27 @@ const SystemSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisablePasswordLoginChanged = async (value: boolean) => {
|
||||
if (value) {
|
||||
showDisablePasswordLoginDialog();
|
||||
} else {
|
||||
showCommonDialog({
|
||||
title: t("setting.system-section.enable-password-login"),
|
||||
content: t("setting.system-section.enable-password-login-warning"),
|
||||
style: "warning",
|
||||
dialogName: "enable-password-login-dialog",
|
||||
onConfirm: async () => {
|
||||
setState({ ...state, disablePasswordLogin: value });
|
||||
globalStore.setSystemStatus({ disablePasswordLogin: value });
|
||||
await api.upsertSystemSetting({
|
||||
name: "disable-password-login",
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCustomizedProfileButtonClick = () => {
|
||||
showUpdateCustomizedProfileDialog();
|
||||
};
|
||||
@ -241,6 +267,10 @@ const SystemSection = () => {
|
||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.disable-password-login")}</span>
|
||||
<Switch checked={state.disablePasswordLogin} onChange={(event) => handleDisablePasswordLoginChanged(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.disable-public-memos")}</span>
|
||||
<Switch checked={state.disablePublicMemos} onChange={(event) => handleDisablePublicMemosChanged(event.target.checked)} />
|
||||
|
@ -256,6 +256,11 @@
|
||||
},
|
||||
"database-file-size": "Database File Size",
|
||||
"allow-user-signup": "Allow user signup",
|
||||
"disable-password-login": "Disable password login",
|
||||
"disable-password-login-warning": "This will disable password login for all users. It is not possible to log in without reverting this setting in the database if your configured identity providers fail. You'll also have to be extra carefull when removing an identity provider❗",
|
||||
"disable-password-login-final-warning": "Please type \"CONFIRM\" if you know what you are doing.",
|
||||
"enable-password-login": "Enable password login",
|
||||
"enable-password-login-warning": "This will enable password login for all users. Continue only if you want to users to be able to log in using both SSO and password❗",
|
||||
"ignore-version-upgrade": "Ignore version upgrade",
|
||||
"disable-public-memos": "Disable public memos",
|
||||
"max-upload-size": "Maximum upload size (MiB)",
|
||||
@ -300,7 +305,8 @@
|
||||
"authorization-endpoint": "Authorization endpoint",
|
||||
"token-endpoint": "Token endpoint",
|
||||
"user-endpoint": "User endpoint",
|
||||
"scopes": "Scopes"
|
||||
"scopes": "Scopes",
|
||||
"disabled-password-login-warning": "Password-login is disabled, be extra careful when removing identity providers❗"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
@ -381,7 +387,9 @@
|
||||
"update-succeed": "Update succeeded",
|
||||
"page-not-found": "404 - Page Not Found 😥",
|
||||
"maximum-upload-size-is": "Maximum allowed upload size is {{size}} MiB",
|
||||
"file-exceeds-upload-limit-of": "File {{file}} exceeds upload limit of {{size}} MiB"
|
||||
"file-exceeds-upload-limit-of": "File {{file}} exceeds upload limit of {{size}} MiB",
|
||||
"updating-setting-failed": "Updating setting failed",
|
||||
"password-login-disabled": "Can't remove last identity provider when password login is disabled"
|
||||
},
|
||||
"days": {
|
||||
"mon": "Mon",
|
||||
|
@ -19,6 +19,7 @@ const Auth = () => {
|
||||
const mode = systemStatus.profile.mode;
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const disablePasswordLogin = systemStatus.disablePasswordLogin;
|
||||
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -135,50 +136,52 @@ const Auth = () => {
|
||||
<img className="h-20 w-auto rounded-full shadow mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
||||
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
|
||||
</div>
|
||||
<form className="w-full mt-4" onSubmit={handleFormSubmit}>
|
||||
<div className="flex flex-col justify-start items-start w-full gap-4">
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="text"
|
||||
placeholder={t("common.username")}
|
||||
value={username}
|
||||
onChange={handleUsernameInputChanged}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder={t("common.password")}
|
||||
value={password}
|
||||
onChange={handlePasswordInputChanged}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center w-full mt-6">
|
||||
{actionBtnLoadingState.isLoading && <Icon.Loader className="w-4 h-auto mr-2 animate-spin dark:text-gray-300" />}
|
||||
{!systemStatus.host ? (
|
||||
<Button disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
|
||||
{t("common.sign-up")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{systemStatus?.allowSignUp && (
|
||||
<>
|
||||
<Button variant={"plain"} disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
|
||||
{t("common.sign-up")}
|
||||
</Button>
|
||||
<span className="mr-2 font-mono text-gray-200">/</span>
|
||||
</>
|
||||
)}
|
||||
<Button type="submit" disabled={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>
|
||||
{t("common.sign-in")}
|
||||
{!disablePasswordLogin && (
|
||||
<form className="w-full mt-4" onSubmit={handleFormSubmit}>
|
||||
<div className="flex flex-col justify-start items-start w-full gap-4">
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="text"
|
||||
placeholder={t("common.username")}
|
||||
value={username}
|
||||
onChange={handleUsernameInputChanged}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder={t("common.password")}
|
||||
value={password}
|
||||
onChange={handlePasswordInputChanged}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center w-full mt-6">
|
||||
{actionBtnLoadingState.isLoading && <Icon.Loader className="w-4 h-auto mr-2 animate-spin dark:text-gray-300" />}
|
||||
{!systemStatus.host ? (
|
||||
<Button disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
|
||||
{t("common.sign-up")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
{systemStatus?.allowSignUp && (
|
||||
<>
|
||||
<Button variant={"plain"} disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
|
||||
{t("common.sign-up")}
|
||||
</Button>
|
||||
<span className="mr-2 font-mono text-gray-200">/</span>
|
||||
</>
|
||||
)}
|
||||
<Button type="submit" disabled={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>
|
||||
{t("common.sign-in")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{!systemStatus.host && (
|
||||
<p className="w-full inline-block float-right text-sm mt-4 text-gray-500 text-right whitespace-pre-wrap">
|
||||
{t("auth.host-tip")}
|
||||
@ -186,7 +189,7 @@ const Auth = () => {
|
||||
)}
|
||||
{identityProviderList.length > 0 && (
|
||||
<>
|
||||
<Divider className="!my-4">{t("common.or")}</Divider>
|
||||
{!disablePasswordLogin && <Divider className="!my-4">{t("common.or")}</Divider>}
|
||||
<div className="w-full flex flex-col space-y-2">
|
||||
{identityProviderList.map((identityProvider) => (
|
||||
<Button
|
||||
|
@ -11,6 +11,7 @@ export const initialGlobalState = async () => {
|
||||
appearance: "system" as Appearance,
|
||||
systemStatus: {
|
||||
allowSignUp: false,
|
||||
disablePasswordLogin: false,
|
||||
disablePublicMemos: false,
|
||||
maxUploadSizeMiB: 0,
|
||||
autoBackupInterval: 0,
|
||||
|
@ -19,6 +19,7 @@ const globalSlice = createSlice({
|
||||
},
|
||||
dbSize: 0,
|
||||
allowSignUp: false,
|
||||
disablePasswordLogin: false,
|
||||
disablePublicMemos: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
|
1
web/src/types/modules/system.d.ts
vendored
1
web/src/types/modules/system.d.ts
vendored
@ -18,6 +18,7 @@ interface SystemStatus {
|
||||
dbSize: number;
|
||||
// System settings
|
||||
allowSignUp: boolean;
|
||||
disablePasswordLogin: boolean;
|
||||
disablePublicMemos: boolean;
|
||||
maxUploadSizeMiB: number;
|
||||
autoBackupInterval: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user