Admin Dashboard overview

This commit is contained in:
Roman 2024-04-24 16:02:16 +02:00
parent 18bb6bffa7
commit d38ad87220
9 changed files with 111 additions and 18 deletions

@ -21,7 +21,7 @@ namespace Core\API {
"allowed_extensions" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING), "allowed_extensions" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING),
"trusted_domains" => new ArrayType("trusted_domains", Parameter::TYPE_STRING), "trusted_domains" => new ArrayType("trusted_domains", Parameter::TYPE_STRING),
"user_registration_enabled" => new Parameter("user_registration_enabled", Parameter::TYPE_BOOLEAN), "user_registration_enabled" => new Parameter("user_registration_enabled", Parameter::TYPE_BOOLEAN),
"captcha_provider" => new StringType("captcha_provider", -1, true, "none", CaptchaProvider::PROVIDERS), "captcha_provider" => new StringType("captcha_provider", -1, true, "disabled", CaptchaProvider::PROVIDERS),
"mail_enabled" => new Parameter("mail_enabled", Parameter::TYPE_BOOLEAN), "mail_enabled" => new Parameter("mail_enabled", Parameter::TYPE_BOOLEAN),
"mail_port" => new IntegerType("mail_port", 1, 65535), "mail_port" => new IntegerType("mail_port", 1, 65535),
"rate_limiting_enabled" => new Parameter("rate_limiting_enabled", Parameter::TYPE_BOOLEAN), "rate_limiting_enabled" => new Parameter("rate_limiting_enabled", Parameter::TYPE_BOOLEAN),

@ -46,11 +46,12 @@ class Stats extends Request {
"server" => [ "server" => [
"version" => WEBBASE_VERSION, "version" => WEBBASE_VERSION,
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown", "server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
"memory_usage" => memory_get_usage(), "memoryUsage" => memory_get_usage(),
"load_avg" => $loadAvg, "loadAverage" => $loadAvg,
"database" => $this->context->getSQL()->getStatus(), "database" => $this->context->getSQL()->getStatus(),
"mail" => $settings->isMailEnabled(), "mail" => $settings->isMailEnabled(),
"captcha" => $settings->getCaptchaProvider()?->jsonSerialize() "captcha" => $settings->getCaptchaProvider()?->jsonSerialize(),
"rateLimiting" => $settings->isRateLimitingEnabled()
], ],
]; ];

@ -1470,6 +1470,10 @@ namespace Core\API\User {
'token' => new StringType('token', 36), 'token' => new StringType('token', 36),
)); ));
$this->userToken = null; $this->userToken = null;
$this->rateLimiting = new RateLimiting(
new RateLimitRule(10, 30, RateLimitRule::SECOND),
new RateLimitRule(30, 1, RateLimitRule::MINUTE),
);
} }
public function getToken(): ?UserToken { public function getToken(): ?UserToken {

@ -109,7 +109,7 @@ class Settings {
$settings->timeZone = date_default_timezone_get(); $settings->timeZone = date_default_timezone_get();
// captcha // captcha
$settings->captchaProvider = "none"; $settings->captchaProvider = "disabled";
$settings->captchaSiteKey = ""; $settings->captchaSiteKey = "";
$settings->captchaSecretKey = ""; $settings->captchaSecretKey = "";

@ -3,6 +3,8 @@
return [ return [
"title" => "Einstellungen", "title" => "Einstellungen",
"information" => "Informationen", "information" => "Informationen",
"disabled" => "Deaktiviert",
"enabled" => "Aktiviert",
# API Key # API Key
"api_key" => "API Schlüssel", "api_key" => "API Schlüssel",
@ -44,6 +46,8 @@ return [
"captcha_provider" => "Captcha Anbieter", "captcha_provider" => "Captcha Anbieter",
"captcha_site_key" => "Öffentlicher Captcha Schlüssel", "captcha_site_key" => "Öffentlicher Captcha Schlüssel",
"captcha_secret_key" => "Geheimer Captcha Schlüssel", "captcha_secret_key" => "Geheimer Captcha Schlüssel",
"recaptcha" => "Google reCaptcha",
"hcaptcha" => "hCaptcha",
# redis # redis
"rate_limit" => "Rate-Limit", "rate_limit" => "Rate-Limit",

@ -3,6 +3,8 @@
return [ return [
"title" => "Settings", "title" => "Settings",
"information" => "Information", "information" => "Information",
"disabled" => "Disabled",
"enabled" => "Enabled",
# API Key # API Key
"api_key" => "API Key", "api_key" => "API Key",
@ -44,6 +46,8 @@ return [
"captcha_provider" => "Captcha Provider", "captcha_provider" => "Captcha Provider",
"captcha_site_key" => "Captcha Site Key", "captcha_site_key" => "Captcha Site Key",
"captcha_secret_key" => "Secret Captcha Key", "captcha_secret_key" => "Secret Captcha Key",
"recaptcha" => "Google reCaptcha",
"hcaptcha" => "hCaptcha",
# redis # redis
"rate_limit" => "Rate Limiting", "rate_limit" => "Rate Limiting",

@ -6,11 +6,11 @@ use Core\Objects\ApiObject;
abstract class CaptchaProvider extends ApiObject { abstract class CaptchaProvider extends ApiObject {
const NONE = "none"; const DISABLED = "disabled";
const RECAPTCHA = "recaptcha"; const RECAPTCHA = "recaptcha";
const HCAPTCHA = "hcaptcha"; const HCAPTCHA = "hcaptcha";
const PROVIDERS = [self::NONE, self::RECAPTCHA, self::HCAPTCHA]; const PROVIDERS = [self::DISABLED, self::RECAPTCHA, self::HCAPTCHA];
private string $siteKey; private string $siteKey;
private string $secretKey; private string $secretKey;

@ -2,8 +2,18 @@ import * as React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {useCallback, useContext, useEffect, useState} from "react"; import {useCallback, useContext, useEffect, useState} from "react";
import {LocaleContext} from "shared/locale"; import {LocaleContext} from "shared/locale";
import {ArrowCircleRight, BugReport, Groups, LibraryBooks, People} from "@mui/icons-material"; import {humanReadableSize} from "shared/util";
import {CircularProgress} from "@mui/material"; import {sprintf} from "sprintf-js";
import {
ArrowCircleRight,
BugReport,
CheckCircle,
Groups,
HighlightOff,
LibraryBooks,
People
} from "@mui/icons-material";
import {Box, CircularProgress, Paper, Table, TableBody, TableCell, TableRow} from "@mui/material";
const StatBox = (props) => <div className={"col-lg-3 col-6"}> const StatBox = (props) => <div className={"col-lg-3 col-6"}>
<div className={"small-box bg-" + props.color}> <div className={"small-box bg-" + props.color}>
@ -24,6 +34,19 @@ const StatBox = (props) => <div className={"col-lg-3 col-6"}>
</div> </div>
</div> </div>
const StatusLine = (props) => {
const {enabled, text, ...other} = props;
if (enabled) {
return <Box display="grid" gridTemplateColumns={"30px auto"}>
<CheckCircle color={"primary"} title={text} /> {text}
</Box>
} else {
return <Box display="grid" gridTemplateColumns={"30px auto"}>
<HighlightOff color={"secondary"} title={text} /> {text}
</Box>
}
}
export default function Overview(props) { export default function Overview(props) {
const [fetchStats, setFetchStats] = useState(true); const [fetchStats, setFetchStats] = useState(true);
@ -32,7 +55,7 @@ export default function Overview(props) {
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general", "admin"], currentLocale).then(data => { requestModules(props.api, ["general", "admin", "settings"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg); props.showDialog("Error fetching translations: " + data.msg);
} }
@ -56,12 +79,10 @@ export default function Overview(props) {
onFetchStats(); onFetchStats();
}, []); }, []);
/* let loadAvg = stats ? stats.server.loadAverage : null;
let loadAvg = this.state.server.load_avg; if (Array.isArray(loadAvg)) {
if (Array.isArray(this.state.server.load_avg)) { loadAvg = loadAvg.map(v => sprintf("%.1f", v)).join(", ");
loadAvg = this.state.server.load_avg.join(" ");
} }
*/
return <> return <>
<div className={"content-header"}> <div className={"content-header"}>
@ -100,6 +121,59 @@ export default function Overview(props) {
link={"/admin/logs"} /> link={"/admin/logs"} />
</div> </div>
</div> </div>
<Box m={2} p={2} component={Paper}>
<h4>Server Stats</h4><hr />
{stats === null ? <CircularProgress /> :
<Table>
<TableBody>
<TableRow>
<TableCell>Web-Base Version</TableCell>
<TableCell>{stats.server.version}</TableCell>
</TableRow>
<TableRow>
<TableCell>Server</TableCell>
<TableCell>{stats.server.server ?? "Unknown"}</TableCell>
</TableRow>
<TableRow>
<TableCell>Load Average</TableCell>
<TableCell>{loadAvg}</TableCell>
</TableRow>
<TableRow>
<TableCell>Memory Usage</TableCell>
<TableCell>{humanReadableSize(stats.server.memoryUsage)}</TableCell>
</TableRow>
<TableRow>
<TableCell>Database</TableCell>
<TableCell>{stats.server.database}</TableCell>
</TableRow>
<TableRow>
<TableCell>Captcha</TableCell>
<TableCell>
<StatusLine enabled={!!stats.server.captcha}
text={L("settings." + (stats.server.captcha ? stats.server.captcha.name : "disabled"))}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Mail</TableCell>
<TableCell>
<StatusLine enabled={!!stats.server.mail}
text={L("settings." + (stats.server.mail ? "enabled" : "disabled"))}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Rate-Limiting</TableCell>
<TableCell>
<StatusLine enabled={!!stats.server.rateLimiting}
text={L("settings." + (stats.server.rateLimiting ? "enabled" : "disabled"))}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
}
</Box>
</section> </section>
</> </>
} }

@ -283,10 +283,16 @@ export default function SettingsView(props) {
</FormGroup> </FormGroup>
]; ];
} else if (selectedTab === "captcha") { } else if (selectedTab === "captcha") {
let captchaOptions = {};
["disabled", "recaptcha", "hcaptcha"].reduce((map, key) => {
map[key] = L("settings." + key);
return map;
}, captchaOptions);
return [ return [
renderSelection("captcha_provider", {"none": L("settings.none"), "recaptcha": "Google reCaptcha", "hcaptcha": "hCaptcha"}), renderSelection("captcha_provider", captchaOptions),
renderTextInput("captcha_site_key", settings.captcha_provider === "none"), renderTextInput("captcha_site_key", settings.captcha_provider === "disabled"),
renderPasswordInput("captcha_secret_key", settings.captcha_provider === "none"), renderPasswordInput("captcha_secret_key", settings.captcha_provider === "disabled"),
]; ];
} else if (selectedTab === "redis") { } else if (selectedTab === "redis") {
return [ return [