mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add server configuration management.
This commit is contained in:
@@ -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>;
|
||||
|
22
frontend/src/components/input.tsx
Normal file
22
frontend/src/components/input.tsx
Normal 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>
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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>
|
||||
|
92
frontend/src/pages/manage_server_config_page.tsx
Normal file
92
frontend/src/pages/manage_server_config_page.tsx
Normal 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>
|
||||
}
|
@@ -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)
|
||||
}}/>
|
||||
|
Reference in New Issue
Block a user