Add server configuration management.

This commit is contained in:
2025-05-14 21:50:59 +08:00
parent 703812d3df
commit 5c08ab34ea
16 changed files with 337 additions and 29 deletions

View File

@@ -1,8 +1,17 @@
export function ErrorAlert({message, className}: {message: string, className?: string}) {
export function ErrorAlert({ message, className }: { message: string, className?: string }) {
return <div role="alert" className={`alert alert-error ${className}`}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{message}</span>
</div>;
}
export function InfoAlert({ message, className }: { message: string, className?: string }) {
return <div role="alert" className={`alert alert-info ${className}`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>{message}</span>
</div>;

View File

@@ -0,0 +1,22 @@
interface InputProps {
type?: string;
placeholder?: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
label: string;
inlineLabel?: boolean;
}
export default function Input(props: InputProps) {
if (props.inlineLabel) {
return <label className="input w-full">
{props.label}
<input type={props.type} className="grow" placeholder={props.placeholder} value={props.value} onChange={props.onChange} />
</label>
} else {
return <fieldset className="fieldset w-full">
<legend className="fieldset-legend">{props.label}</legend>
<input type={props.type} className="input w-full" placeholder={props.placeholder} value={props.value} onChange={props.onChange} />
</fieldset>
}
}

View File

@@ -108,3 +108,12 @@ export interface CommentWithResource {
user: User;
resource: Resource;
}
export interface ServerConfig {
max_uploading_size_in_mb: number;
max_file_size_in_mb: number;
max_downloads_per_day_for_single_ip: number;
allow_register: boolean;
cloudflare_turnstile_site_key: string;
cloudflare_turnstile_secret_key: string;
}

View File

@@ -13,7 +13,8 @@ import {
User,
UserWithToken,
Comment,
CommentWithResource
CommentWithResource,
ServerConfig
} from "./models.ts";
class Network {
@@ -635,6 +636,32 @@ class Network {
return { success: false, message: e.toString() };
}
}
async getServerConfig(): Promise<Response<ServerConfig>> {
try {
const response = await axios.get(`${this.apiBaseUrl}/config`);
return response.data;
} catch (e: any) {
console.error(e);
return {
success: false,
message: e.toString(),
};
}
}
async setServerConfig(config: ServerConfig): Promise<Response<void>> {
try {
const response = await axios.post(`${this.apiBaseUrl}/config`, config);
return response.data;
} catch (e: any) {
console.error(e);
return {
success: false,
message: e.toString(),
};
}
}
}
export const network = new Network();

View File

@@ -4,6 +4,7 @@ import StorageView from "./manage_storage_page.tsx";
import UserView from "./manage_user_page.tsx";
import { useTranslation } from "react-i18next";
import { ManageMePage } from "./manage_me_page.tsx";
import ManageServerConfigPage from "./manage_server_config_page.tsx";
export default function ManagePage() {
const { t } = useTranslation();
@@ -43,13 +44,15 @@ export default function ManagePage() {
const pageNames = [
t("My Info"),
t("Storage"),
t("Users")
t("Users"),
t("Server"),
]
const pageComponents = [
<ManageMePage/>,
<StorageView/>,
<UserView/>
<UserView/>,
<ManageServerConfigPage/>,
]
return <div className="drawer lg:drawer-open">
@@ -80,6 +83,7 @@ export default function ManagePage() {
{buildItem(t("My Info"), <MdOutlineBadge className={"text-xl"}/>, 0)}
{buildItem(t("Storage"), <MdOutlineStorage className={"text-xl"}/>, 1)}
{buildItem(t("Users"), <MdOutlinePerson className={"text-xl"}/>, 2)}
{buildItem(t("Server"), <MdOutlineStorage className={"text-xl"}/>, 3)}
</ul>
</div>
</div>

View File

@@ -0,0 +1,92 @@
import { useTranslation } from "react-i18next";
import { app } from "../app"
import { ErrorAlert, InfoAlert } from "../components/alert"
import { useEffect, useState } from "react";
import { ServerConfig } from "../network/models";
import Loading from "../components/loading";
import Input from "../components/input";
import { network } from "../network/network";
import showToast from "../components/toast";
import Button from "../components/button";
export default function ManageServerConfigPage() {
const { t } = useTranslation();
const [config, setConfig] = useState<ServerConfig | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
network.getServerConfig().then((res) => {
if (res.success) {
setConfig(res.data!);
} else {
showToast({
message: res.message,
type: "error",
})
}
})
}, []);
if (!app.user) {
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} />
}
if (!app.user?.is_admin) {
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} />
}
if (config == null) {
return <Loading />
}
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isLoading) {
return;
}
setIsLoading(true);
const res = await network.setServerConfig(config);
if (res.success) {
showToast({
message: t("Update server config successfully"),
type: "success",
});
} else {
showToast({
message: res.message,
type: "error",
});
}
setIsLoading(false);
};
return <form className="px-4" onSubmit={handleSubmit}>
<Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => {
setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) })
}}></Input>
<Input type="number" value={config.max_file_size_in_mb.toString()} label="Max file size (MB)" onChange={(e) => {
setConfig({...config, max_file_size_in_mb: parseInt(e.target.value) })
}}></Input>
<Input type="number" value={config.max_downloads_per_day_for_single_ip.toString()} label="Max downloads per day for single IP" onChange={(e) => {
setConfig({...config, max_downloads_per_day_for_single_ip: parseInt(e.target.value) })
}}></Input>
<fieldset className="fieldset w-full">
<legend className="fieldset-legend">Allow register</legend>
<input type="checkbox" checked={config.allow_register} className="toggle-primary toggle" onChange={(e) => {
setConfig({ ...config, allow_register: e.target.checked })
}} />
</fieldset>
<Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => {
setConfig({...config, cloudflare_turnstile_site_key: e.target.value })
}}></Input>
<Input type="text" value={config.cloudflare_turnstile_secret_key} label="Cloudflare Turnstile Secret Key" onChange={(e) => {
setConfig({...config, cloudflare_turnstile_secret_key: e.target.value })
}}></Input>
<InfoAlert className="my-2" message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download." />
<div className="flex justify-end">
<Button className="btn-accent shadow" isLoading={isLoading}>{t("Submit")}</Button>
</div>
</form>
}

View File

@@ -96,7 +96,7 @@ export default function ResourcePage() {
}
</p>
<div className="tabs tabs-box my-4 mx-2 p-4">
<label className="tab">
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
setPage(0)
}}/>
@@ -109,7 +109,7 @@ export default function ResourcePage() {
<Article article={resource.article}/>
</div>
<label className="tab">
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
setPage(1)
}}/>
@@ -122,7 +122,7 @@ export default function ResourcePage() {
<Files files={resource.files} resourceID={resource.id}/>
</div>
<label className="tab">
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
setPage(2)
}}/>