diff --git a/Core/API/Request.class.php b/Core/API/Request.class.php index a404567..8d445a1 100644 --- a/Core/API/Request.class.php +++ b/Core/API/Request.class.php @@ -278,6 +278,7 @@ abstract class Request { $this->success = $success; } } catch (\Error $err) { + http_response_code(500); $this->createError($err->getMessage()); $this->logger->error($err->getMessage()); } diff --git a/Core/API/Stats.class.php b/Core/API/Stats.class.php index e8be77a..589941d 100644 --- a/Core/API/Stats.class.php +++ b/Core/API/Stats.class.php @@ -57,6 +57,7 @@ class Stats extends Request { $sql = $this->context->getSQL(); $userCount = User::count($sql); $pageCount = Route::count($sql, new CondBool("active")); + $groupCount = Group::count($sql); $req = new \Core\API\Visitors\Stats($this->context); $this->success = $req->execute(array("type"=>"monthly")); $this->lastError = $req->getLastError(); @@ -82,6 +83,7 @@ class Stats extends Request { $this->result["data"] = [ "userCount" => $userCount, "pageCount" => $pageCount, + "groupCount" => $groupCount, "visitors" => $visitorStatistics, "visitorsTotal" => $visitorCount, "server" => [ diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php index a08883f..d8ea8c9 100644 --- a/Core/API/UserAPI.class.php +++ b/Core/API/UserAPI.class.php @@ -576,26 +576,26 @@ namespace Core\API\User { if ($user !== false) { if ($user === null) { return $this->wrongCredentials(); - } else { - if (password_verify($password, $user->password)) { - if (!$user->confirmed) { - $this->result["emailConfirmed"] = false; - return $this->createError("Your email address has not been confirmed yet."); - } else if (!($session = $this->context->createSession($user, $stayLoggedIn))) { - return $this->createError("Error creating Session: " . $sql->getLastError()); - } else { - $tfaToken = $user->getTwoFactorToken(); - - $this->result["loggedIn"] = true; - $this->result["user"] = $user->jsonSerialize(); - $this->result["session"] = $session->jsonSerialize(); - $this->result["logoutIn"] = $session->getExpiresSeconds(); - $this->check2FA($tfaToken); - $this->success = true; - } + } else if (!$user->isActive()) { + return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake."); + } else if (password_verify($password, $user->password)) { + if (!$user->confirmed) { + $this->result["emailConfirmed"] = false; + return $this->createError("Your email address has not been confirmed yet."); + } else if (!($session = $this->context->createSession($user, $stayLoggedIn))) { + return $this->createError("Error creating Session: " . $sql->getLastError()); } else { - return $this->wrongCredentials(); + $tfaToken = $user->getTwoFactorToken(); + + $this->result["loggedIn"] = true; + $this->result["user"] = $user->jsonSerialize(); + $this->result["session"] = $session->jsonSerialize(); + $this->result["logoutIn"] = $session->getExpiresSeconds(); + $this->check2FA($tfaToken); + $this->success = true; } + } else { + return $this->wrongCredentials(); } } else { return $this->createError("Error fetching user details: " . $sql->getLastError()); @@ -934,43 +934,47 @@ namespace Core\API\User { if ($user === false) { return $this->createError("Could not fetch user details: " . $sql->getLastError()); } else if ($user !== null) { - $validHours = 1; - $token = generateRandomString(36); - $userToken = new UserToken($user, $token, UserToken::TYPE_PASSWORD_RESET, $validHours); - if (!$userToken->save($sql)) { - return $this->createError("Could not create user token: " . $sql->getLastError()); - } + if (!$user->isActive()) { + return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake."); + } else { + $validHours = 1; + $token = generateRandomString(36); + $userToken = new UserToken($user, $token, UserToken::TYPE_PASSWORD_RESET, $validHours); + if (!$userToken->save($sql)) { + return $this->createError("Could not create user token: " . $sql->getLastError()); + } - $baseUrl = $settings->getBaseUrl(); - $siteName = $settings->getSiteName(); + $baseUrl = $settings->getBaseUrl(); + $siteName = $settings->getSiteName(); - $req = new Render($this->context); - $this->success = $req->execute([ - "file" => "mail/reset_password.twig", - "parameters" => [ - "link" => "$baseUrl/resetPassword?token=$token", - "site_name" => $siteName, - "base_url" => $baseUrl, - "username" => $user->name, - "valid_time" => $this->formatDuration($validHours, "hour") - ] - ]); - $this->lastError = $req->getLastError(); + $req = new Render($this->context); + $this->success = $req->execute([ + "file" => "mail/reset_password.twig", + "parameters" => [ + "link" => "$baseUrl/resetPassword?token=$token", + "site_name" => $siteName, + "base_url" => $baseUrl, + "username" => $user->name, + "valid_time" => $this->formatDuration($validHours, "hour") + ] + ]); + $this->lastError = $req->getLastError(); - if ($this->success) { - $messageBody = $req->getResult()["html"]; + if ($this->success) { + $messageBody = $req->getResult()["html"]; - $gpgKey = $user->getGPG(); - $gpgFingerprint = ($gpgKey && $gpgKey->isConfirmed()) ? $gpgKey->getFingerprint() : null; - $request = new \Core\API\Mail\Send($this->context); - $this->success = $request->execute(array( - "to" => $email, - "subject" => "[$siteName] Password Reset", - "body" => $messageBody, - "gpgFingerprint" => $gpgFingerprint - )); - $this->lastError = $request->getLastError(); - $this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]); + $gpgKey = $user->getGPG(); + $gpgFingerprint = ($gpgKey && $gpgKey->isConfirmed()) ? $gpgKey->getFingerprint() : null; + $request = new \Core\API\Mail\Send($this->context); + $this->success = $request->execute(array( + "to" => $email, + "subject" => "[$siteName] Password Reset", + "body" => $messageBody, + "gpgFingerprint" => $gpgFingerprint + )); + $this->lastError = $request->getLastError(); + $this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]); + } } } diff --git a/Core/Localization/de_DE/account.php b/Core/Localization/de_DE/account.php index c012567..efe2a38 100644 --- a/Core/Localization/de_DE/account.php +++ b/Core/Localization/de_DE/account.php @@ -22,6 +22,7 @@ return [ "reset_password_form_title" => "Ein neues Passwort wählen", "reset_password_request_form_title" => "Geben Sie Ihre E-Mail Adresse ein um ein Passwort-Reset Token zu erhalten", "form_title" => "Bitte geben Sie ihre Daten ein", + "name" => "Name", "username" => "Benutzername", "username_or_email" => "Benutzername oder E-Mail", "email" => "E-Mail Adresse", @@ -60,5 +61,11 @@ return [ "registered_at" => "Registriert am", "last_online" => "Zuletzt online", "groups" => "Gruppen", + "group_name" => "Gruppenname", + "new_group" => "Neue Gruppe", + "members" => "Mitglieder", + "member_count" => "Mitgliederanzahl", + "color" => "Farbe", "logged_in_as" => "Eingeloggt als", + "active" => "Aktiv", ]; \ No newline at end of file diff --git a/Core/Localization/de_DE/admin.php b/Core/Localization/de_DE/admin.php index d1af028..ea9bcd3 100644 --- a/Core/Localization/de_DE/admin.php +++ b/Core/Localization/de_DE/admin.php @@ -12,4 +12,12 @@ return [ "acl" => "Zugriffsberechtigung", "logs" => "Logs", "help" => "Hilfe", + + # Dashboard + "users_registered" => "Benutzer registriert", + "available_groups" => "verfügbare Gruppen", + "routes_defined" => "Routen definiert", + + # Dialogs + "fetch_stats_error" => "Fehler beim Holen der Stats", ]; \ No newline at end of file diff --git a/Core/Localization/de_DE/logs.php b/Core/Localization/de_DE/logs.php index 05666aa..d6df6c5 100644 --- a/Core/Localization/de_DE/logs.php +++ b/Core/Localization/de_DE/logs.php @@ -10,6 +10,8 @@ return [ "search_query" => "Suchanfrage", "no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen", "timestamp_placeholder" => "Datum und Zeitpunk Auswählen zum Filtern", + "hide_details" => "Details verstecken", + "show_details" => "Details zeigen", // dialog "fetch_log_error" => "Fehler beim Holen der Log-Einträge", diff --git a/Core/Localization/en_US/account.php b/Core/Localization/en_US/account.php index 58a689e..d08617d 100644 --- a/Core/Localization/en_US/account.php +++ b/Core/Localization/en_US/account.php @@ -22,6 +22,7 @@ return [ "reset_password_form_title" => "Choose a new password", "reset_password_request_form_title" => "Enter your E-Mail address, to receive a password reset token.", "form_title" => "Please fill with your details", + "name" => "Name", "username" => "Username", "username_or_email" => "Username or E-Mail", "email" => "E-Mail Address", @@ -60,5 +61,11 @@ return [ "registered_at" => "Registered At", "last_online" => "Last Online", "groups" => "Groups", + "group_name" => "Group Name", + "new_group" => "New Group", + "members" => "Members", + "member_count" => "Member Count", + "color" => "Color", "logged_in_as" => "Logged in as", + "active" => "Active", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/admin.php b/Core/Localization/en_US/admin.php index 68a12e9..14269fd 100644 --- a/Core/Localization/en_US/admin.php +++ b/Core/Localization/en_US/admin.php @@ -12,4 +12,12 @@ return [ "acl" => "Access Control", "logs" => "Logs", "help" => "Help", + + # Dashboard + "users_registered" => "Users registered", + "available_groups" => "available Groups", + "routes_defined" => "Routes defined", + + # Dialogs + "fetch_stats_error" => "Error fetching stats", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php index 3da41ec..a45ab35 100644 --- a/Core/Localization/en_US/general.php +++ b/Core/Localization/en_US/general.php @@ -59,7 +59,6 @@ return [ "move" => "Move", "overwrite" => "Overwrite", - # data table "showing_x_of_y_entries" => "Showing %d of %d entries", "controls" => "Controls", diff --git a/Core/Localization/en_US/logs.php b/Core/Localization/en_US/logs.php index b9c23f8..6990b02 100644 --- a/Core/Localization/en_US/logs.php +++ b/Core/Localization/en_US/logs.php @@ -10,6 +10,8 @@ return [ "search_query" => "Search query", "no_entries_placeholder" => "No log entries to display", "timestamp_placeholder" => "Select date and time to filter", + "hide_details" => "Hide details", + "show_details" => "Show details", // dialog "fetch_log_error" => "Error fetching log entries", diff --git a/Core/Objects/Context.class.php b/Core/Objects/Context.class.php index 346196a..94778cf 100644 --- a/Core/Objects/Context.class.php +++ b/Core/Objects/Context.class.php @@ -176,8 +176,9 @@ class Context { ->addJoin(new InnerJoin("ApiKey", "ApiKey.user_id", "User.id")) ->whereEq("ApiKey.api_key", $apiKey) ->whereGt("valid_until", $this->sql->currentTimestamp()) - ->whereTrue("ApiKey.active", true) - ->whereTrue("User.confirmed", true) + ->whereTrue("ApiKey.active") + ->whereTrue("User.confirmed") + ->whereTrue("User.active") ->fetchEntities()); return $this->user !== null; diff --git a/Core/Objects/DatabaseEntity/Session.class.php b/Core/Objects/DatabaseEntity/Session.class.php index d40eeb9..e553361 100644 --- a/Core/Objects/DatabaseEntity/Session.class.php +++ b/Core/Objects/DatabaseEntity/Session.class.php @@ -46,10 +46,16 @@ class Session extends DatabaseEntity { ->whereEq("Session.uuid", $sessionUUID) ->whereTrue("Session.active") ->whereGt("Session.expires", $sql->now())); + if (!$session) { return null; } + $user = $session->getUser(); + if (!$user->isActive() || !$user->isConfirmed()) { + return null; + } + if (is_array($session->data)) { foreach ($session->data as $key => $value) { $_SESSION[$key] = $value; diff --git a/Core/Objects/DatabaseEntity/User.class.php b/Core/Objects/DatabaseEntity/User.class.php index 37899a3..ed499d0 100644 --- a/Core/Objects/DatabaseEntity/User.class.php +++ b/Core/Objects/DatabaseEntity/User.class.php @@ -45,6 +45,10 @@ class User extends DatabaseEntity { #[DefaultValue(false)] public bool $confirmed; + #[Visibility(Visibility::BY_GROUP, Group::ADMIN, Group::SUPPORT)] + #[DefaultValue(true)] + public bool $active; + #[DefaultValue(Language::AMERICAN_ENGLISH)] public Language $language; #[Visibility(Visibility::BY_GROUP, Group::ADMIN, Group::SUPPORT)] @@ -92,6 +96,14 @@ class User extends DatabaseEntity { return $this->profilePicture; } + public function isActive():bool { + return $this->active; + } + + public function isConfirmed():bool { + return $this->confirmed; + } + public function __debugInfo(): array { return [ 'id' => $this->getId(), diff --git a/index.php b/index.php index 2f2505c..170e37d 100644 --- a/index.php +++ b/index.php @@ -65,7 +65,13 @@ if ($installation) { is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) { $response = $router->returnStatusCode(intval($_GET["error"])); } else { - $response = $router->run($requestedUri); + try { + $response = $router->run($requestedUri); + } catch (\Error $e) { + http_response_code(500); + $router->getLogger()->error($e->getMessage()); + $router->returnStatusCode(500); + } } } diff --git a/react/admin-panel/src/views/group/group-edit.js b/react/admin-panel/src/views/group/group-edit.js index a185098..7386f47 100644 --- a/react/admin-panel/src/views/group/group-edit.js +++ b/react/admin-panel/src/views/group/group-edit.js @@ -56,7 +56,7 @@ export default function EditGroupView(props) {
  1. Home
  2. -
  3. Group
  4. +
  5. {L("account.group")}
  6. { isNewGroup ? L("general.new") : groupId }
@@ -68,13 +68,15 @@ export default function EditGroupView(props) {
this.submitForm(e)}>
- +
- +
-  Back +  {L("general.go_back")} - +
-

Members

+

{L("account.members")}

diff --git a/react/admin-panel/src/views/group/group-list.js b/react/admin-panel/src/views/group/group-list.js index 8c10825..224c243 100644 --- a/react/admin-panel/src/views/group/group-list.js +++ b/react/admin-panel/src/views/group/group-list.js @@ -52,8 +52,8 @@ export default function GroupListView(props) { const columnDefinitions = [ new NumericColumn(L("general.id"), "id"), - new StringColumn(L("group.name"), "name"), - new NumericColumn(L("group.member_count"), "memberCount"), + new StringColumn(L("account.name"), "name"), + new NumericColumn(L("account.member_count"), "memberCount"), actionColumn, ]; @@ -66,7 +66,7 @@ export default function GroupListView(props) {
  1. Home
  2. -
  3. Groups
  4. +
  5. {L("account.groups")}
diff --git a/react/admin-panel/src/views/log-view.js b/react/admin-panel/src/views/log-view.js index e04ae8c..0320956 100644 --- a/react/admin-panel/src/views/log-view.js +++ b/react/admin-panel/src/views/log-view.js @@ -9,7 +9,8 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import {API_DATETIME_FORMAT} from "shared/constants"; import {format, toDate} from "date-fns"; -import {FormControl, FormGroup, FormLabel, MenuItem, Select} from "@material-ui/core"; +import {Box, FormControl, FormGroup, FormLabel, IconButton, MenuItem, Select} from "@material-ui/core"; +import {ExpandLess, ExpandMore} from "@material-ui/icons"; export default function LogView(props) { @@ -59,6 +60,16 @@ export default function LogView(props) { }); }, [api, showDialog, logLevel, timestamp, query]); + const onToggleDetails = useCallback(entry => { + let newLogEntries = [...logEntries]; + for (const logEntry of newLogEntries) { + if (logEntry.id === entry.id) { + logEntry.showDetails = !logEntry.showDetails; + } + } + setLogEntries(newLogEntries); + }, [logEntries]); + useEffect(() => { // TODO: wait for user to finish typing before force reloading setForceReload(forceReload + 1); @@ -68,7 +79,19 @@ export default function LogView(props) { let column = new DataColumn(L("logs.message"), "message"); column.sortable = false; column.renderData = (L, entry) => { - return
{entry.message}
+ return + + onToggleDetails(entry)} + title={L(entry.showDetails ? "logs.hide_details" : "logs.show_details")}> + {entry.showDetails ? : } + + + +
+                            {entry.showDetails ? entry.message : entry.message.split("\n")[0]}
+                        
+
+
} return column; })(); diff --git a/react/admin-panel/src/views/overview.js b/react/admin-panel/src/views/overview.js index 9989750..64c8f95 100644 --- a/react/admin-panel/src/views/overview.js +++ b/react/admin-panel/src/views/overview.js @@ -1,13 +1,45 @@ import * as React from "react"; import {Link} from "react-router-dom"; import {format, getDaysInMonth} from "date-fns"; -import {CircularProgress, Icon} from "@material-ui/core"; -import {useCallback, useEffect, useState} from "react"; +import {CircularProgress} from "@material-ui/core"; +import {useCallback, useContext, useEffect, useState} from "react"; +import {LocaleContext} from "shared/locale"; +import {LibraryBooks, People} from "@material-ui/icons"; +import {ArrowCircleRight, Groups} from "@mui/icons-material"; + +const StatBox = (props) =>
+
+
+ {props.count ? + <> +

{props.count}

+

{props.text}

+ : + } +
+
+ {props.icon} +
+ + More info + +
+
export default function Overview(props) { const [fetchStats, setFetchStats] = useState(true); const [stats, setStats] = useState(null); + const {translate: L, currentLocale, requestModules} = useContext(LocaleContext); + + + useEffect(() => { + requestModules(props.api, ["general", "admin"], currentLocale).then(data => { + if (!data.success) { + props.showDialog("Error fetching translations: " + data.msg); + } + }); + }, [currentLocale]); const onFetchStats = useCallback((force = false) => { if (force || fetchStats) { @@ -16,7 +48,7 @@ export default function Overview(props) { if (res.success) { setStats(res.data); } else { - props.showDialog("Error fetching stats: " + res.msg, "Error fetching stats"); + props.showDialog(res.msg, L("admin.fetch_stats_error")); } }); } @@ -73,12 +105,12 @@ export default function Overview(props) {
-

Dashboard

+

{L("admin.dashboard")}

  1. Home
  2. -
  3. Dashboard
  4. +
  5. {L("admin.dashboard")}
@@ -87,24 +119,18 @@ export default function Overview(props) {
-
-
-
- {stats ? - <> -

{stats.userCount}

-

Users registered

- : - } -
-
- -
- - More info - -
-
+ } + link={"/admin/users"} /> + } + link={"/admin/users"} /> + } + link={"/admin/routes"} />
diff --git a/react/admin-panel/src/views/user/user-list.js b/react/admin-panel/src/views/user/user-list.js index 8fa2510..aa3620f 100644 --- a/react/admin-panel/src/views/user/user-list.js +++ b/react/admin-panel/src/views/user/user-list.js @@ -5,7 +5,7 @@ import { BoolColumn, ControlsColumn, DataColumn, - DataTable, + DataTable, DateTimeColumn, NumericColumn, StringColumn } from "shared/elements/data-table"; @@ -48,7 +48,11 @@ export default function UserListView(props) { const groupColumn = (() => { let column = new DataColumn(L("account.groups"), "groups"); column.renderData = (L, entry) => { - return Object.values(entry.groups).map(group => ) + return Object.values(entry.groups).map(group => + ) } return column; })(); @@ -59,6 +63,8 @@ export default function UserListView(props) { new StringColumn(L("account.full_name"), "fullName"), new StringColumn(L("account.email"), "email"), groupColumn, + new DateTimeColumn(L("account.registered_at"), "registeredAt"), + new BoolColumn(L("account.active"), "active", { align: "center" }), new BoolColumn(L("account.confirmed"), "confirmed", { align: "center" }), new ControlsColumn(L("general.controls"), [ { label: L("general.edit"), element: EditIcon, onClick: (entry) => navigate(`/admin/user/${entry.id}`) } diff --git a/react/shared/elements/data-table.js b/react/shared/elements/data-table.js index 537f254..49d8722 100644 --- a/react/shared/elements/data-table.js +++ b/react/shared/elements/data-table.js @@ -83,7 +83,11 @@ export function DataTable(props) { title={L("general.sort_by") + ": " + column.label} onClick={() => onChangeSort(index, column)} align={column.align}> - {sortColumn === index ? (sortAscending ? : ): <>}{column.renderHead(index)} + {sortColumn === index ? + (sortAscending ? : ) : + <> + } + {column.renderHead(index)} ); } else { headerRow.push(