frontend, added user active flag, localization
This commit is contained in:
parent
9fc0a19f59
commit
0125c83bea
@ -278,6 +278,7 @@ abstract class Request {
|
|||||||
$this->success = $success;
|
$this->success = $success;
|
||||||
}
|
}
|
||||||
} catch (\Error $err) {
|
} catch (\Error $err) {
|
||||||
|
http_response_code(500);
|
||||||
$this->createError($err->getMessage());
|
$this->createError($err->getMessage());
|
||||||
$this->logger->error($err->getMessage());
|
$this->logger->error($err->getMessage());
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ class Stats extends Request {
|
|||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$userCount = User::count($sql);
|
$userCount = User::count($sql);
|
||||||
$pageCount = Route::count($sql, new CondBool("active"));
|
$pageCount = Route::count($sql, new CondBool("active"));
|
||||||
|
$groupCount = Group::count($sql);
|
||||||
$req = new \Core\API\Visitors\Stats($this->context);
|
$req = new \Core\API\Visitors\Stats($this->context);
|
||||||
$this->success = $req->execute(array("type"=>"monthly"));
|
$this->success = $req->execute(array("type"=>"monthly"));
|
||||||
$this->lastError = $req->getLastError();
|
$this->lastError = $req->getLastError();
|
||||||
@ -82,6 +83,7 @@ class Stats extends Request {
|
|||||||
$this->result["data"] = [
|
$this->result["data"] = [
|
||||||
"userCount" => $userCount,
|
"userCount" => $userCount,
|
||||||
"pageCount" => $pageCount,
|
"pageCount" => $pageCount,
|
||||||
|
"groupCount" => $groupCount,
|
||||||
"visitors" => $visitorStatistics,
|
"visitors" => $visitorStatistics,
|
||||||
"visitorsTotal" => $visitorCount,
|
"visitorsTotal" => $visitorCount,
|
||||||
"server" => [
|
"server" => [
|
||||||
|
@ -576,26 +576,26 @@ namespace Core\API\User {
|
|||||||
if ($user !== false) {
|
if ($user !== false) {
|
||||||
if ($user === null) {
|
if ($user === null) {
|
||||||
return $this->wrongCredentials();
|
return $this->wrongCredentials();
|
||||||
} else {
|
} else if (!$user->isActive()) {
|
||||||
if (password_verify($password, $user->password)) {
|
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
||||||
if (!$user->confirmed) {
|
} else if (password_verify($password, $user->password)) {
|
||||||
$this->result["emailConfirmed"] = false;
|
if (!$user->confirmed) {
|
||||||
return $this->createError("Your email address has not been confirmed yet.");
|
$this->result["emailConfirmed"] = false;
|
||||||
} else if (!($session = $this->context->createSession($user, $stayLoggedIn))) {
|
return $this->createError("Your email address has not been confirmed yet.");
|
||||||
return $this->createError("Error creating Session: " . $sql->getLastError());
|
} else if (!($session = $this->context->createSession($user, $stayLoggedIn))) {
|
||||||
} else {
|
return $this->createError("Error creating Session: " . $sql->getLastError());
|
||||||
$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 {
|
} 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 {
|
} else {
|
||||||
return $this->createError("Error fetching user details: " . $sql->getLastError());
|
return $this->createError("Error fetching user details: " . $sql->getLastError());
|
||||||
@ -934,43 +934,47 @@ namespace Core\API\User {
|
|||||||
if ($user === false) {
|
if ($user === false) {
|
||||||
return $this->createError("Could not fetch user details: " . $sql->getLastError());
|
return $this->createError("Could not fetch user details: " . $sql->getLastError());
|
||||||
} else if ($user !== null) {
|
} else if ($user !== null) {
|
||||||
$validHours = 1;
|
if (!$user->isActive()) {
|
||||||
$token = generateRandomString(36);
|
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
||||||
$userToken = new UserToken($user, $token, UserToken::TYPE_PASSWORD_RESET, $validHours);
|
} else {
|
||||||
if (!$userToken->save($sql)) {
|
$validHours = 1;
|
||||||
return $this->createError("Could not create user token: " . $sql->getLastError());
|
$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();
|
$baseUrl = $settings->getBaseUrl();
|
||||||
$siteName = $settings->getSiteName();
|
$siteName = $settings->getSiteName();
|
||||||
|
|
||||||
$req = new Render($this->context);
|
$req = new Render($this->context);
|
||||||
$this->success = $req->execute([
|
$this->success = $req->execute([
|
||||||
"file" => "mail/reset_password.twig",
|
"file" => "mail/reset_password.twig",
|
||||||
"parameters" => [
|
"parameters" => [
|
||||||
"link" => "$baseUrl/resetPassword?token=$token",
|
"link" => "$baseUrl/resetPassword?token=$token",
|
||||||
"site_name" => $siteName,
|
"site_name" => $siteName,
|
||||||
"base_url" => $baseUrl,
|
"base_url" => $baseUrl,
|
||||||
"username" => $user->name,
|
"username" => $user->name,
|
||||||
"valid_time" => $this->formatDuration($validHours, "hour")
|
"valid_time" => $this->formatDuration($validHours, "hour")
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
$this->lastError = $req->getLastError();
|
$this->lastError = $req->getLastError();
|
||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$messageBody = $req->getResult()["html"];
|
$messageBody = $req->getResult()["html"];
|
||||||
|
|
||||||
$gpgKey = $user->getGPG();
|
$gpgKey = $user->getGPG();
|
||||||
$gpgFingerprint = ($gpgKey && $gpgKey->isConfirmed()) ? $gpgKey->getFingerprint() : null;
|
$gpgFingerprint = ($gpgKey && $gpgKey->isConfirmed()) ? $gpgKey->getFingerprint() : null;
|
||||||
$request = new \Core\API\Mail\Send($this->context);
|
$request = new \Core\API\Mail\Send($this->context);
|
||||||
$this->success = $request->execute(array(
|
$this->success = $request->execute(array(
|
||||||
"to" => $email,
|
"to" => $email,
|
||||||
"subject" => "[$siteName] Password Reset",
|
"subject" => "[$siteName] Password Reset",
|
||||||
"body" => $messageBody,
|
"body" => $messageBody,
|
||||||
"gpgFingerprint" => $gpgFingerprint
|
"gpgFingerprint" => $gpgFingerprint
|
||||||
));
|
));
|
||||||
$this->lastError = $request->getLastError();
|
$this->lastError = $request->getLastError();
|
||||||
$this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
|
$this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ return [
|
|||||||
"reset_password_form_title" => "Ein neues Passwort wählen",
|
"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",
|
"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",
|
"form_title" => "Bitte geben Sie ihre Daten ein",
|
||||||
|
"name" => "Name",
|
||||||
"username" => "Benutzername",
|
"username" => "Benutzername",
|
||||||
"username_or_email" => "Benutzername oder E-Mail",
|
"username_or_email" => "Benutzername oder E-Mail",
|
||||||
"email" => "E-Mail Adresse",
|
"email" => "E-Mail Adresse",
|
||||||
@ -60,5 +61,11 @@ return [
|
|||||||
"registered_at" => "Registriert am",
|
"registered_at" => "Registriert am",
|
||||||
"last_online" => "Zuletzt online",
|
"last_online" => "Zuletzt online",
|
||||||
"groups" => "Gruppen",
|
"groups" => "Gruppen",
|
||||||
|
"group_name" => "Gruppenname",
|
||||||
|
"new_group" => "Neue Gruppe",
|
||||||
|
"members" => "Mitglieder",
|
||||||
|
"member_count" => "Mitgliederanzahl",
|
||||||
|
"color" => "Farbe",
|
||||||
"logged_in_as" => "Eingeloggt als",
|
"logged_in_as" => "Eingeloggt als",
|
||||||
|
"active" => "Aktiv",
|
||||||
];
|
];
|
@ -12,4 +12,12 @@ return [
|
|||||||
"acl" => "Zugriffsberechtigung",
|
"acl" => "Zugriffsberechtigung",
|
||||||
"logs" => "Logs",
|
"logs" => "Logs",
|
||||||
"help" => "Hilfe",
|
"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",
|
||||||
];
|
];
|
@ -10,6 +10,8 @@ return [
|
|||||||
"search_query" => "Suchanfrage",
|
"search_query" => "Suchanfrage",
|
||||||
"no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen",
|
"no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen",
|
||||||
"timestamp_placeholder" => "Datum und Zeitpunk Auswählen zum Filtern",
|
"timestamp_placeholder" => "Datum und Zeitpunk Auswählen zum Filtern",
|
||||||
|
"hide_details" => "Details verstecken",
|
||||||
|
"show_details" => "Details zeigen",
|
||||||
|
|
||||||
// dialog
|
// dialog
|
||||||
"fetch_log_error" => "Fehler beim Holen der Log-Einträge",
|
"fetch_log_error" => "Fehler beim Holen der Log-Einträge",
|
||||||
|
@ -22,6 +22,7 @@ return [
|
|||||||
"reset_password_form_title" => "Choose a new password",
|
"reset_password_form_title" => "Choose a new password",
|
||||||
"reset_password_request_form_title" => "Enter your E-Mail address, to receive a password reset token.",
|
"reset_password_request_form_title" => "Enter your E-Mail address, to receive a password reset token.",
|
||||||
"form_title" => "Please fill with your details",
|
"form_title" => "Please fill with your details",
|
||||||
|
"name" => "Name",
|
||||||
"username" => "Username",
|
"username" => "Username",
|
||||||
"username_or_email" => "Username or E-Mail",
|
"username_or_email" => "Username or E-Mail",
|
||||||
"email" => "E-Mail Address",
|
"email" => "E-Mail Address",
|
||||||
@ -60,5 +61,11 @@ return [
|
|||||||
"registered_at" => "Registered At",
|
"registered_at" => "Registered At",
|
||||||
"last_online" => "Last Online",
|
"last_online" => "Last Online",
|
||||||
"groups" => "Groups",
|
"groups" => "Groups",
|
||||||
|
"group_name" => "Group Name",
|
||||||
|
"new_group" => "New Group",
|
||||||
|
"members" => "Members",
|
||||||
|
"member_count" => "Member Count",
|
||||||
|
"color" => "Color",
|
||||||
"logged_in_as" => "Logged in as",
|
"logged_in_as" => "Logged in as",
|
||||||
|
"active" => "Active",
|
||||||
];
|
];
|
@ -12,4 +12,12 @@ return [
|
|||||||
"acl" => "Access Control",
|
"acl" => "Access Control",
|
||||||
"logs" => "Logs",
|
"logs" => "Logs",
|
||||||
"help" => "Help",
|
"help" => "Help",
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
"users_registered" => "Users registered",
|
||||||
|
"available_groups" => "available Groups",
|
||||||
|
"routes_defined" => "Routes defined",
|
||||||
|
|
||||||
|
# Dialogs
|
||||||
|
"fetch_stats_error" => "Error fetching stats",
|
||||||
];
|
];
|
@ -59,7 +59,6 @@ return [
|
|||||||
"move" => "Move",
|
"move" => "Move",
|
||||||
"overwrite" => "Overwrite",
|
"overwrite" => "Overwrite",
|
||||||
|
|
||||||
|
|
||||||
# data table
|
# data table
|
||||||
"showing_x_of_y_entries" => "Showing %d of %d entries",
|
"showing_x_of_y_entries" => "Showing %d of %d entries",
|
||||||
"controls" => "Controls",
|
"controls" => "Controls",
|
||||||
|
@ -10,6 +10,8 @@ return [
|
|||||||
"search_query" => "Search query",
|
"search_query" => "Search query",
|
||||||
"no_entries_placeholder" => "No log entries to display",
|
"no_entries_placeholder" => "No log entries to display",
|
||||||
"timestamp_placeholder" => "Select date and time to filter",
|
"timestamp_placeholder" => "Select date and time to filter",
|
||||||
|
"hide_details" => "Hide details",
|
||||||
|
"show_details" => "Show details",
|
||||||
|
|
||||||
// dialog
|
// dialog
|
||||||
"fetch_log_error" => "Error fetching log entries",
|
"fetch_log_error" => "Error fetching log entries",
|
||||||
|
@ -176,8 +176,9 @@ class Context {
|
|||||||
->addJoin(new InnerJoin("ApiKey", "ApiKey.user_id", "User.id"))
|
->addJoin(new InnerJoin("ApiKey", "ApiKey.user_id", "User.id"))
|
||||||
->whereEq("ApiKey.api_key", $apiKey)
|
->whereEq("ApiKey.api_key", $apiKey)
|
||||||
->whereGt("valid_until", $this->sql->currentTimestamp())
|
->whereGt("valid_until", $this->sql->currentTimestamp())
|
||||||
->whereTrue("ApiKey.active", true)
|
->whereTrue("ApiKey.active")
|
||||||
->whereTrue("User.confirmed", true)
|
->whereTrue("User.confirmed")
|
||||||
|
->whereTrue("User.active")
|
||||||
->fetchEntities());
|
->fetchEntities());
|
||||||
|
|
||||||
return $this->user !== null;
|
return $this->user !== null;
|
||||||
|
@ -46,10 +46,16 @@ class Session extends DatabaseEntity {
|
|||||||
->whereEq("Session.uuid", $sessionUUID)
|
->whereEq("Session.uuid", $sessionUUID)
|
||||||
->whereTrue("Session.active")
|
->whereTrue("Session.active")
|
||||||
->whereGt("Session.expires", $sql->now()));
|
->whereGt("Session.expires", $sql->now()));
|
||||||
|
|
||||||
if (!$session) {
|
if (!$session) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = $session->getUser();
|
||||||
|
if (!$user->isActive() || !$user->isConfirmed()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_array($session->data)) {
|
if (is_array($session->data)) {
|
||||||
foreach ($session->data as $key => $value) {
|
foreach ($session->data as $key => $value) {
|
||||||
$_SESSION[$key] = $value;
|
$_SESSION[$key] = $value;
|
||||||
|
@ -45,6 +45,10 @@ class User extends DatabaseEntity {
|
|||||||
#[DefaultValue(false)]
|
#[DefaultValue(false)]
|
||||||
public bool $confirmed;
|
public bool $confirmed;
|
||||||
|
|
||||||
|
#[Visibility(Visibility::BY_GROUP, Group::ADMIN, Group::SUPPORT)]
|
||||||
|
#[DefaultValue(true)]
|
||||||
|
public bool $active;
|
||||||
|
|
||||||
#[DefaultValue(Language::AMERICAN_ENGLISH)] public Language $language;
|
#[DefaultValue(Language::AMERICAN_ENGLISH)] public Language $language;
|
||||||
|
|
||||||
#[Visibility(Visibility::BY_GROUP, Group::ADMIN, Group::SUPPORT)]
|
#[Visibility(Visibility::BY_GROUP, Group::ADMIN, Group::SUPPORT)]
|
||||||
@ -92,6 +96,14 @@ class User extends DatabaseEntity {
|
|||||||
return $this->profilePicture;
|
return $this->profilePicture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isActive():bool {
|
||||||
|
return $this->active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConfirmed():bool {
|
||||||
|
return $this->confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
public function __debugInfo(): array {
|
public function __debugInfo(): array {
|
||||||
return [
|
return [
|
||||||
'id' => $this->getId(),
|
'id' => $this->getId(),
|
||||||
|
@ -65,7 +65,13 @@ if ($installation) {
|
|||||||
is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) {
|
is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) {
|
||||||
$response = $router->returnStatusCode(intval($_GET["error"]));
|
$response = $router->returnStatusCode(intval($_GET["error"]));
|
||||||
} else {
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export default function EditGroupView(props) {
|
|||||||
<div className={"col-sm-6"}>
|
<div className={"col-sm-6"}>
|
||||||
<ol className={"breadcrumb float-sm-right"}>
|
<ol className={"breadcrumb float-sm-right"}>
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||||
<li className="breadcrumb-item active"><Link to={"/admin/groups"}>Group</Link></li>
|
<li className="breadcrumb-item active"><Link to={"/admin/groups"}>{L("account.group")}</Link></li>
|
||||||
<li className="breadcrumb-item active">{ isNewGroup ? L("general.new") : groupId }</li>
|
<li className="breadcrumb-item active">{ isNewGroup ? L("general.new") : groupId }</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@ -68,13 +68,15 @@ export default function EditGroupView(props) {
|
|||||||
<div className={"col-6 pl-5 pr-5"}>
|
<div className={"col-6 pl-5 pr-5"}>
|
||||||
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
|
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
|
||||||
<div className={"form-group"}>
|
<div className={"form-group"}>
|
||||||
<label htmlFor={"name"}>Group Name</label>
|
<label htmlFor={"name"}>{L("account.group_name")}</label>
|
||||||
<input type={"text"} className={"form-control"} placeholder={"Name"}
|
<input type={"text"} className={"form-control"} placeholder={"Name"}
|
||||||
name={"name"} id={"name"} maxLength={32} value={group.name}/>
|
name={"name"} id={"name"} maxLength={32} value={group.name}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={"form-group"}>
|
<div className={"form-group"}>
|
||||||
<label htmlFor={"color"}>Color</label>
|
<label htmlFor={"color"}>
|
||||||
|
{L("account.color")}
|
||||||
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={group.color}
|
value={group.color}
|
||||||
@ -88,13 +90,15 @@ export default function EditGroupView(props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={"/admin/groups"} className={"btn btn-info mt-2 mr-2"}>
|
<Link to={"/admin/groups"} className={"btn btn-info mt-2 mr-2"}>
|
||||||
Back
|
{L("general.go_back")}
|
||||||
</Link>
|
</Link>
|
||||||
<button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
|
<button type={"submit"} className={"btn btn-primary mt-2"}>
|
||||||
|
{L("general.submit")}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className={"col-6"}>
|
<div className={"col-6"}>
|
||||||
<h3>Members</h3>
|
<h3>{L("account.members")}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,8 +52,8 @@ export default function GroupListView(props) {
|
|||||||
|
|
||||||
const columnDefinitions = [
|
const columnDefinitions = [
|
||||||
new NumericColumn(L("general.id"), "id"),
|
new NumericColumn(L("general.id"), "id"),
|
||||||
new StringColumn(L("group.name"), "name"),
|
new StringColumn(L("account.name"), "name"),
|
||||||
new NumericColumn(L("group.member_count"), "memberCount"),
|
new NumericColumn(L("account.member_count"), "memberCount"),
|
||||||
actionColumn,
|
actionColumn,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ export default function GroupListView(props) {
|
|||||||
<div className={"col-sm-6"}>
|
<div className={"col-sm-6"}>
|
||||||
<ol className={"breadcrumb float-sm-right"}>
|
<ol className={"breadcrumb float-sm-right"}>
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||||
<li className="breadcrumb-item active">Groups</li>
|
<li className="breadcrumb-item active">{L("account.groups")}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,8 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
|||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import {API_DATETIME_FORMAT} from "shared/constants";
|
import {API_DATETIME_FORMAT} from "shared/constants";
|
||||||
import {format, toDate} from "date-fns";
|
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) {
|
export default function LogView(props) {
|
||||||
|
|
||||||
@ -59,6 +60,16 @@ export default function LogView(props) {
|
|||||||
});
|
});
|
||||||
}, [api, showDialog, logLevel, timestamp, query]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// TODO: wait for user to finish typing before force reloading
|
// TODO: wait for user to finish typing before force reloading
|
||||||
setForceReload(forceReload + 1);
|
setForceReload(forceReload + 1);
|
||||||
@ -68,7 +79,19 @@ export default function LogView(props) {
|
|||||||
let column = new DataColumn(L("logs.message"), "message");
|
let column = new DataColumn(L("logs.message"), "message");
|
||||||
column.sortable = false;
|
column.sortable = false;
|
||||||
column.renderData = (L, entry) => {
|
column.renderData = (L, entry) => {
|
||||||
return <pre>{entry.message}</pre>
|
return <Box display={"grid"} gridTemplateColumns={"40px auto"}>
|
||||||
|
<Box alignSelf={"top"} textAlign={"center"}>
|
||||||
|
<IconButton size={"small"} onClick={() => onToggleDetails(entry)}
|
||||||
|
title={L(entry.showDetails ? "logs.hide_details" : "logs.show_details")}>
|
||||||
|
{entry.showDetails ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box alignSelf={"center"}>
|
||||||
|
<pre>
|
||||||
|
{entry.showDetails ? entry.message : entry.message.split("\n")[0]}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
return column;
|
return column;
|
||||||
})();
|
})();
|
||||||
|
@ -1,13 +1,45 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {format, getDaysInMonth} from "date-fns";
|
import {format, getDaysInMonth} from "date-fns";
|
||||||
import {CircularProgress, Icon} from "@material-ui/core";
|
import {CircularProgress} from "@material-ui/core";
|
||||||
import {useCallback, useEffect, useState} from "react";
|
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) => <div className={"col-lg-3 col-6"}>
|
||||||
|
<div className={"small-box bg-" + props.color}>
|
||||||
|
<div className={"inner"}>
|
||||||
|
{props.count ?
|
||||||
|
<>
|
||||||
|
<h3>{props.count}</h3>
|
||||||
|
<p>{props.text}</p>
|
||||||
|
</> : <CircularProgress variant={"determinate"} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={"icon"}>
|
||||||
|
{props.icon}
|
||||||
|
</div>
|
||||||
|
<Link to={props.link} className={"small-box-footer text-right p-1"}>
|
||||||
|
More info <ArrowCircleRight />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
export default function Overview(props) {
|
export default function Overview(props) {
|
||||||
|
|
||||||
const [fetchStats, setFetchStats] = useState(true);
|
const [fetchStats, setFetchStats] = useState(true);
|
||||||
const [stats, setStats] = useState(null);
|
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) => {
|
const onFetchStats = useCallback((force = false) => {
|
||||||
if (force || fetchStats) {
|
if (force || fetchStats) {
|
||||||
@ -16,7 +48,7 @@ export default function Overview(props) {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
setStats(res.data);
|
setStats(res.data);
|
||||||
} else {
|
} 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) {
|
|||||||
<div className={"container-fluid"}>
|
<div className={"container-fluid"}>
|
||||||
<div className={"row mb-2"}>
|
<div className={"row mb-2"}>
|
||||||
<div className={"col-sm-6"}>
|
<div className={"col-sm-6"}>
|
||||||
<h1 className={"m-0 text-dark"}>Dashboard</h1>
|
<h1 className={"m-0 text-dark"}>{L("admin.dashboard")}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className={"col-sm-6"}>
|
<div className={"col-sm-6"}>
|
||||||
<ol className={"breadcrumb float-sm-right"}>
|
<ol className={"breadcrumb float-sm-right"}>
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||||
<li className="breadcrumb-item active">Dashboard</li>
|
<li className="breadcrumb-item active">{L("admin.dashboard")}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -87,24 +119,18 @@ export default function Overview(props) {
|
|||||||
<section className={"content"}>
|
<section className={"content"}>
|
||||||
<div className={"container-fluid"}>
|
<div className={"container-fluid"}>
|
||||||
<div className={"row"}>
|
<div className={"row"}>
|
||||||
<div className={"col-lg-3 col-6"}>
|
<StatBox color={"info"} count={stats?.userCount}
|
||||||
<div className="small-box bg-info">
|
text={L("admin.users_registered")}
|
||||||
<div className={"inner"}>
|
icon={<People />}
|
||||||
{stats ?
|
link={"/admin/users"} />
|
||||||
<>
|
<StatBox color={"success"} count={stats?.groupCount}
|
||||||
<h3>{stats.userCount}</h3>
|
text={L("admin.available_groups")}
|
||||||
<p>Users registered</p>
|
icon={<Groups />}
|
||||||
</> : <CircularProgress variant={"determinate"} />
|
link={"/admin/users"} />
|
||||||
}
|
<StatBox color={"warning"} count={stats?.pageCount}
|
||||||
</div>
|
text={L("admin.routes_defined")}
|
||||||
<div className="icon">
|
icon={<LibraryBooks />}
|
||||||
<Icon icon={"users"} />
|
link={"/admin/routes"} />
|
||||||
</div>
|
|
||||||
<Link to={"/admin/users"} className="small-box-footer">
|
|
||||||
More info <Icon icon={"arrow-circle-right"}/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
BoolColumn,
|
BoolColumn,
|
||||||
ControlsColumn,
|
ControlsColumn,
|
||||||
DataColumn,
|
DataColumn,
|
||||||
DataTable,
|
DataTable, DateTimeColumn,
|
||||||
NumericColumn,
|
NumericColumn,
|
||||||
StringColumn
|
StringColumn
|
||||||
} from "shared/elements/data-table";
|
} from "shared/elements/data-table";
|
||||||
@ -48,7 +48,11 @@ export default function UserListView(props) {
|
|||||||
const groupColumn = (() => {
|
const groupColumn = (() => {
|
||||||
let column = new DataColumn(L("account.groups"), "groups");
|
let column = new DataColumn(L("account.groups"), "groups");
|
||||||
column.renderData = (L, entry) => {
|
column.renderData = (L, entry) => {
|
||||||
return Object.values(entry.groups).map(group => <Chip key={"group-" + group.id} label={group.name}/>)
|
return Object.values(entry.groups).map(group => <Chip
|
||||||
|
key={"group-" + group.id}
|
||||||
|
style={{ backgroundColor: group.color }}
|
||||||
|
label={group.name} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return column;
|
return column;
|
||||||
})();
|
})();
|
||||||
@ -59,6 +63,8 @@ export default function UserListView(props) {
|
|||||||
new StringColumn(L("account.full_name"), "fullName"),
|
new StringColumn(L("account.full_name"), "fullName"),
|
||||||
new StringColumn(L("account.email"), "email"),
|
new StringColumn(L("account.email"), "email"),
|
||||||
groupColumn,
|
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 BoolColumn(L("account.confirmed"), "confirmed", { align: "center" }),
|
||||||
new ControlsColumn(L("general.controls"), [
|
new ControlsColumn(L("general.controls"), [
|
||||||
{ label: L("general.edit"), element: EditIcon, onClick: (entry) => navigate(`/admin/user/${entry.id}`) }
|
{ label: L("general.edit"), element: EditIcon, onClick: (entry) => navigate(`/admin/user/${entry.id}`) }
|
||||||
|
@ -83,7 +83,11 @@ export function DataTable(props) {
|
|||||||
title={L("general.sort_by") + ": " + column.label}
|
title={L("general.sort_by") + ": " + column.label}
|
||||||
onClick={() => onChangeSort(index, column)}
|
onClick={() => onChangeSort(index, column)}
|
||||||
align={column.align}>
|
align={column.align}>
|
||||||
{sortColumn === index ? (sortAscending ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />): <></>}{column.renderHead(index)}
|
{sortColumn === index ?
|
||||||
|
(sortAscending ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />) :
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
{column.renderHead(index)}
|
||||||
</TableCell>);
|
</TableCell>);
|
||||||
} else {
|
} else {
|
||||||
headerRow.push(<TableCell key={"col-" + index} align={column.align}>
|
headerRow.push(<TableCell key={"col-" + index} align={column.align}>
|
||||||
|
Loading…
Reference in New Issue
Block a user