mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat(system): support for disabling public memos (#1003)
* feat(system): support for disabling public memos * fix(web/editor): set visibility to private on disabled public memos * feat(server/memo): find/check if public memos are disabled * fix(server/memo): handle error for finding system error * fix(server/memo): unmarshal visiblity when getting system settings * chore(web): move side effect imports to end * Update memo.go --------- Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
@ -10,6 +10,8 @@ type SystemStatus struct {
|
|||||||
// System settings
|
// System settings
|
||||||
// Allow sign up.
|
// Allow sign up.
|
||||||
AllowSignUp bool `json:"allowSignUp"`
|
AllowSignUp bool `json:"allowSignUp"`
|
||||||
|
// Disable public memos.
|
||||||
|
DisablePublicMemos bool `json:"disablePublicMemos"`
|
||||||
// Additional style.
|
// Additional style.
|
||||||
AdditionalStyle string `json:"additionalStyle"`
|
AdditionalStyle string `json:"additionalStyle"`
|
||||||
// Additional script.
|
// Additional script.
|
||||||
|
@ -17,6 +17,8 @@ const (
|
|||||||
SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
|
SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
|
||||||
// SystemSettingAllowSignUpName is the key type of allow signup setting.
|
// SystemSettingAllowSignUpName is the key type of allow signup setting.
|
||||||
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
|
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
|
||||||
|
// SystemSettingsDisablePublicMemos is the key type of disable public memos setting.
|
||||||
|
SystemSettingDisablePublicMemosName SystemSettingName = "disablePublicMemos"
|
||||||
// SystemSettingAdditionalStyleName is the key type of additional style.
|
// SystemSettingAdditionalStyleName is the key type of additional style.
|
||||||
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
||||||
// SystemSettingAdditionalScriptName is the key type of additional script.
|
// SystemSettingAdditionalScriptName is the key type of additional script.
|
||||||
@ -51,6 +53,8 @@ func (key SystemSettingName) String() string {
|
|||||||
return "secretSessionName"
|
return "secretSessionName"
|
||||||
case SystemSettingAllowSignUpName:
|
case SystemSettingAllowSignUpName:
|
||||||
return "allowSignUp"
|
return "allowSignUp"
|
||||||
|
case SystemSettingDisablePublicMemosName:
|
||||||
|
return "disablePublicMemos"
|
||||||
case SystemSettingAdditionalStyleName:
|
case SystemSettingAdditionalStyleName:
|
||||||
return "additionalStyle"
|
return "additionalStyle"
|
||||||
case SystemSettingAdditionalScriptName:
|
case SystemSettingAdditionalScriptName:
|
||||||
@ -65,6 +69,7 @@ func (key SystemSettingName) String() string {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
SystemSettingAllowSignUpValue = []bool{true, false}
|
SystemSettingAllowSignUpValue = []bool{true, false}
|
||||||
|
SystemSettingDisbalePublicMemosValue = []bool{true, false}
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemSetting struct {
|
type SystemSetting struct {
|
||||||
@ -100,6 +105,24 @@ func (upsert SystemSettingUpsert) Validate() error {
|
|||||||
if invalid {
|
if invalid {
|
||||||
return fmt.Errorf("invalid system setting allow signup value")
|
return fmt.Errorf("invalid system setting allow signup value")
|
||||||
}
|
}
|
||||||
|
} else if upsert.Name == SystemSettingDisablePublicMemosName {
|
||||||
|
value := false
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal system setting disable public memos value")
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := true
|
||||||
|
for _, v := range SystemSettingDisbalePublicMemosValue {
|
||||||
|
if value == v {
|
||||||
|
invalid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if invalid {
|
||||||
|
return fmt.Errorf("invalid system setting disable public memos value")
|
||||||
|
}
|
||||||
} else if upsert.Name == SystemSettingAdditionalStyleName {
|
} else if upsert.Name == SystemSettingAdditionalStyleName {
|
||||||
value := ""
|
value := ""
|
||||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||||
|
@ -53,6 +53,25 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find system settings
|
||||||
|
disablePublicMemosSystemSettingKey := api.SystemSettingDisablePublicMemosName
|
||||||
|
disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||||
|
Name: &disablePublicMemosSystemSettingKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
|
||||||
|
}
|
||||||
|
if disablePublicMemosSystemSetting != nil {
|
||||||
|
disablePublicMemosValue := false
|
||||||
|
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemosValue)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||||
|
}
|
||||||
|
if disablePublicMemosValue {
|
||||||
|
memoCreate.Visibility = api.Private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
memoCreate.CreatorID = userID
|
memoCreate.CreatorID = userID
|
||||||
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,6 +46,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
Profile: *s.Profile,
|
Profile: *s.Profile,
|
||||||
DBSize: 0,
|
DBSize: 0,
|
||||||
AllowSignUp: false,
|
AllowSignUp: false,
|
||||||
|
DisablePublicMemos: false,
|
||||||
AdditionalStyle: "",
|
AdditionalStyle: "",
|
||||||
AdditionalScript: "",
|
AdditionalScript: "",
|
||||||
CustomizedProfile: api.CustomizedProfile{
|
CustomizedProfile: api.CustomizedProfile{
|
||||||
@ -75,6 +76,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
|
|
||||||
if systemSetting.Name == api.SystemSettingAllowSignUpName {
|
if systemSetting.Name == api.SystemSettingAllowSignUpName {
|
||||||
systemStatus.AllowSignUp = value.(bool)
|
systemStatus.AllowSignUp = value.(bool)
|
||||||
|
} else if systemSetting.Name == api.SystemSettingDisablePublicMemosName {
|
||||||
|
systemStatus.DisablePublicMemos = value.(bool)
|
||||||
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
|
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
|
||||||
systemStatus.AdditionalStyle = value.(string)
|
systemStatus.AdditionalStyle = value.(string)
|
||||||
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
||||||
|
@ -4,7 +4,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { getMatchedNodes } from "../labs/marked";
|
import { getMatchedNodes } from "../labs/marked";
|
||||||
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
||||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||||
import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "../store/module";
|
import {
|
||||||
|
useEditorStore,
|
||||||
|
useGlobalStore,
|
||||||
|
useLocationStore,
|
||||||
|
useMemoStore,
|
||||||
|
useResourceStore,
|
||||||
|
useTagStore,
|
||||||
|
useUserStore,
|
||||||
|
} from "../store/module";
|
||||||
import * as storage from "../helpers/storage";
|
import * as storage from "../helpers/storage";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
@ -42,6 +50,9 @@ const MemoEditor = () => {
|
|||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const tagStore = useTagStore();
|
const tagStore = useTagStore();
|
||||||
const resourceStore = useResourceStore();
|
const resourceStore = useResourceStore();
|
||||||
|
const {
|
||||||
|
state: { systemStatus },
|
||||||
|
} = useGlobalStore();
|
||||||
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
@ -63,6 +74,12 @@ const MemoEditor = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (systemStatus.disablePublicMemos) {
|
||||||
|
editorStore.setMemoVisibility("PRIVATE");
|
||||||
|
}
|
||||||
|
}, [systemStatus.disablePublicMemos]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]);
|
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]);
|
||||||
if (editingMemoIdCache) {
|
if (editingMemoIdCache) {
|
||||||
@ -484,7 +501,9 @@ const MemoEditor = () => {
|
|||||||
<Selector
|
<Selector
|
||||||
className="visibility-selector"
|
className="visibility-selector"
|
||||||
value={editorState.memoVisibility}
|
value={editorState.memoVisibility}
|
||||||
|
tooltipTitle={t("memo.visibility.disabled")}
|
||||||
dataSource={memoVisibilityOptionSelectorItems}
|
dataSource={memoVisibilityOptionSelectorItems}
|
||||||
|
disabled={systemStatus.disablePublicMemos}
|
||||||
handleValueChanged={handleMemoVisibilityOptionChanged}
|
handleValueChanged={handleMemoVisibilityOptionChanged}
|
||||||
/>
|
/>
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
|
@ -5,11 +5,14 @@ import { useGlobalStore } from "../../store/module";
|
|||||||
import * as api from "../../helpers/api";
|
import * as api from "../../helpers/api";
|
||||||
import toastHelper from "../Toast";
|
import toastHelper from "../Toast";
|
||||||
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||||
|
import { useAppDispatch } from "../../store";
|
||||||
|
import { setGlobalState } from "../../store/reducer/global";
|
||||||
import "@/less/settings/system-section.less";
|
import "@/less/settings/system-section.less";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
dbSize: number;
|
dbSize: number;
|
||||||
allowSignUp: boolean;
|
allowSignUp: boolean;
|
||||||
|
disablePublicMemos: boolean;
|
||||||
additionalStyle: string;
|
additionalStyle: string;
|
||||||
additionalScript: string;
|
additionalScript: string;
|
||||||
}
|
}
|
||||||
@ -32,8 +35,11 @@ const SystemSection = () => {
|
|||||||
allowSignUp: systemStatus.allowSignUp,
|
allowSignUp: systemStatus.allowSignUp,
|
||||||
additionalStyle: systemStatus.additionalStyle,
|
additionalStyle: systemStatus.additionalStyle,
|
||||||
additionalScript: systemStatus.additionalScript,
|
additionalScript: systemStatus.additionalScript,
|
||||||
|
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
globalStore.fetchSystemStatus();
|
globalStore.fetchSystemStatus();
|
||||||
}, []);
|
}, []);
|
||||||
@ -44,6 +50,7 @@ const SystemSection = () => {
|
|||||||
allowSignUp: systemStatus.allowSignUp,
|
allowSignUp: systemStatus.allowSignUp,
|
||||||
additionalStyle: systemStatus.additionalStyle,
|
additionalStyle: systemStatus.additionalStyle,
|
||||||
additionalScript: systemStatus.additionalScript,
|
additionalScript: systemStatus.additionalScript,
|
||||||
|
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||||
});
|
});
|
||||||
}, [systemStatus]);
|
}, [systemStatus]);
|
||||||
|
|
||||||
@ -100,6 +107,19 @@ const SystemSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDisablePublicMemosChanged = async (value: boolean) => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
disablePublicMemos: value,
|
||||||
|
});
|
||||||
|
// Update global store immediately as MemoEditor/Selector is dependent on this value.
|
||||||
|
dispatch(setGlobalState({ systemStatus: { ...systemStatus, disablePublicMemos: value } }));
|
||||||
|
await api.upsertSystemSetting({
|
||||||
|
name: "disablePublicMemos",
|
||||||
|
value: JSON.stringify(value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveAdditionalScript = async () => {
|
const handleSaveAdditionalScript = async () => {
|
||||||
try {
|
try {
|
||||||
await api.upsertSystemSetting({
|
await api.upsertSystemSetting({
|
||||||
@ -133,6 +153,10 @@ const SystemSection = () => {
|
|||||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||||
</div>
|
</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)} />
|
||||||
|
</div>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
||||||
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
||||||
|
@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useToggle from "../../hooks/useToggle";
|
import useToggle from "../../hooks/useToggle";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
|
import { Tooltip } from "@mui/joy";
|
||||||
import "../../less/common/selector.less";
|
import "../../less/common/selector.less";
|
||||||
|
|
||||||
interface SelectorItem {
|
interface SelectorItem {
|
||||||
@ -14,6 +15,8 @@ interface Props {
|
|||||||
value: string;
|
value: string;
|
||||||
dataSource: SelectorItem[];
|
dataSource: SelectorItem[];
|
||||||
handleValueChanged?: (value: string) => void;
|
handleValueChanged?: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltipTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nullItem = {
|
const nullItem = {
|
||||||
@ -22,7 +25,7 @@ const nullItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Selector: React.FC<Props> = (props: Props) => {
|
const Selector: React.FC<Props> = (props: Props) => {
|
||||||
const { className, dataSource, handleValueChanged, value } = props;
|
const { className, dataSource, handleValueChanged, value, disabled, tooltipTitle } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
||||||
|
|
||||||
@ -58,17 +61,29 @@ const Selector: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCurrentValueClick = (event: React.MouseEvent) => {
|
const handleCurrentValueClick = (event: React.MouseEvent) => {
|
||||||
|
if (disabled) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleSelectorStatus();
|
toggleSelectorStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`selector-wrapper ${className ?? ""}`} ref={selectorElRef}>
|
<Tooltip title={tooltipTitle} hidden={!disabled}>
|
||||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
<div className={`selector-wrapper ${className ?? ""} `} ref={selectorElRef}>
|
||||||
|
<div
|
||||||
|
className={`current-value-container ${showSelector ? "active" : ""} ${disabled && "selector-disabled"}`}
|
||||||
|
onClick={handleCurrentValueClick}
|
||||||
|
>
|
||||||
|
{disabled && (
|
||||||
|
<span className="lock-text">
|
||||||
|
<Icon.Lock className="icon-img" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="value-text">{currentItem.text}</span>
|
<span className="value-text">{currentItem.text}</span>
|
||||||
|
{!disabled && (
|
||||||
<span className="arrow-text">
|
<span className="arrow-text">
|
||||||
<Icon.ChevronDown className="icon-img" />
|
<Icon.ChevronDown className="icon-img" />
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
|
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
|
||||||
@ -91,6 +106,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .lock-text {
|
||||||
|
@apply flex flex-row justify-center items-center w-4 shrink-0 mr-1;
|
||||||
|
}
|
||||||
|
|
||||||
> .arrow-text {
|
> .arrow-text {
|
||||||
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
||||||
|
|
||||||
@ -41,4 +45,11 @@
|
|||||||
@apply px-3 py-1 text-sm text-gray-600;
|
@apply px-3 py-1 text-sm text-gray-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .selector-disabled {
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
@apply pointer-events-none;
|
||||||
|
|
||||||
|
@apply bg-gray-200 dark:bg-zinc-700 dark:border-zinc-600 text-gray-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,8 @@
|
|||||||
"visibility": {
|
"visibility": {
|
||||||
"private": "Only visible to you",
|
"private": "Only visible to you",
|
||||||
"protected": "Visible to members",
|
"protected": "Visible to members",
|
||||||
"public": "Everyone can see"
|
"public": "Everyone can see",
|
||||||
|
"disabled": "Public memos are disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"memo-list": {
|
"memo-list": {
|
||||||
@ -180,6 +181,7 @@
|
|||||||
},
|
},
|
||||||
"database-file-size": "Database File Size",
|
"database-file-size": "Database File Size",
|
||||||
"allow-user-signup": "Allow user signup",
|
"allow-user-signup": "Allow user signup",
|
||||||
|
"disable-public-memos": "Disable public memos",
|
||||||
"additional-style": "Additional style",
|
"additional-style": "Additional style",
|
||||||
"additional-script": "Additional script",
|
"additional-script": "Additional script",
|
||||||
"additional-style-placeholder": "Additional CSS codes",
|
"additional-style-placeholder": "Additional CSS codes",
|
||||||
|
@ -9,6 +9,7 @@ export const initialGlobalState = async () => {
|
|||||||
appearance: "system" as Appearance,
|
appearance: "system" as Appearance,
|
||||||
systemStatus: {
|
systemStatus: {
|
||||||
allowSignUp: false,
|
allowSignUp: false,
|
||||||
|
disablePublicMemos: false,
|
||||||
additionalStyle: "",
|
additionalStyle: "",
|
||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
|
@ -19,6 +19,7 @@ const globalSlice = createSlice({
|
|||||||
},
|
},
|
||||||
dbSize: 0,
|
dbSize: 0,
|
||||||
allowSignUp: false,
|
allowSignUp: false,
|
||||||
|
disablePublicMemos: false,
|
||||||
additionalStyle: "",
|
additionalStyle: "",
|
||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
|
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;
|
dbSize: number;
|
||||||
// System settings
|
// System settings
|
||||||
allowSignUp: boolean;
|
allowSignUp: boolean;
|
||||||
|
disablePublicMemos: boolean;
|
||||||
additionalStyle: string;
|
additionalStyle: string;
|
||||||
additionalScript: string;
|
additionalScript: string;
|
||||||
customizedProfile: CustomizedProfile;
|
customizedProfile: CustomizedProfile;
|
||||||
|
Reference in New Issue
Block a user