From 136ad48a5eeedfaf5d8ab5ad9cdb24b67805c0e9 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 18 Jan 2023 14:37:34 +0100 Subject: [PATCH] small changes --- Core/API/ApiKeyAPI.class.php | 33 ++++++---- Core/API/MailAPI.class.php | 2 +- Core/API/TfaAPI.class.php | 15 ++++- Core/API/UserAPI.class.php | 4 +- Core/Driver/SQL/SQL.class.php | 2 +- Core/Localization/de_DE/account.php | 1 + Core/Localization/en_US/account.php | 1 + Core/Objects/DatabaseEntity/ApiKey.class.php | 16 +++-- Core/core.php | 2 +- react/shared/api.js | 67 ++++++++++++++++++-- react/shared/elements/data-table.js | 17 ++++- react/shared/elements/dialog.jsx | 37 +++++------ react/shared/views/login.jsx | 7 +- 13 files changed, 148 insertions(+), 56 deletions(-) diff --git a/Core/API/ApiKeyAPI.class.php b/Core/API/ApiKeyAPI.class.php index 4fc3910..e7842be 100644 --- a/Core/API/ApiKeyAPI.class.php +++ b/Core/API/ApiKeyAPI.class.php @@ -16,6 +16,7 @@ namespace Core\API\ApiKey { use Core\API\ApiKeyAPI; use Core\API\Parameter\Parameter; + use Core\API\Traits\Pagination; use Core\Driver\SQL\Condition\Compare; use Core\Driver\SQL\Condition\CondAnd; use Core\Driver\SQL\Query\Insert; @@ -32,17 +33,16 @@ namespace Core\API\ApiKey { public function _execute(): bool { $sql = $this->context->getSQL(); + $currentUser = $this->context->getUser(); - $apiKey = new ApiKey(); - $apiKey->apiKey = generateRandomString(64); - $apiKey->validUntil = (new \DateTime())->modify("+30 DAY"); - $apiKey->user = $this->context->getUser(); - + $apiKey = ApiKey::create($currentUser); $this->success = $apiKey->save($sql); $this->lastError = $sql->getLastError(); if ($this->success) { - $this->result["api_key"] = $apiKey->jsonSerialize(); + $this->result["apiKey"] = $apiKey->jsonSerialize( + ["id", "validUntil", "token", "active"] + ); } return $this->success; @@ -55,10 +55,13 @@ namespace Core\API\ApiKey { class Fetch extends ApiKeyAPI { + use Pagination; + public function __construct(Context $context, $externalCall = false) { - parent::__construct($context, $externalCall, array( - "showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true) - )); + $params = $this->getPaginationParameters(["token", "validUntil", "active"]); + $params["showActiveOnly"] = new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true); + + parent::__construct($context, $externalCall, $params); $this->loginRequired = true; } @@ -74,14 +77,18 @@ namespace Core\API\ApiKey { ); } - $apiKeys = ApiKey::findAll($sql, $condition); - $this->success = ($apiKeys !== FALSE); + if (!$this->initPagination($sql, ApiKey::class, $condition)) { + return false; + } + + $apiKeys = $this->createPaginationQuery($sql)->execute(); + $this->success = ($apiKeys !== FALSE && $apiKeys !== null); $this->lastError = $sql->getLastError(); if ($this->success) { - $this->result["api_keys"] = array(); + $this->result["apiKeys"] = []; foreach($apiKeys as $apiKey) { - $this->result["api_keys"][$apiKey->getId()] = $apiKey->jsonSerialize(); + $this->result["apiKeys"][] = $apiKey->jsonSerialize(); } } diff --git a/Core/API/MailAPI.class.php b/Core/API/MailAPI.class.php index 32aa935..a3cf2e8 100644 --- a/Core/API/MailAPI.class.php +++ b/Core/API/MailAPI.class.php @@ -20,7 +20,7 @@ namespace Core\API { $settings = $req->getResult()["settings"]; if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") { - $this->createError("Mail is not configured yet."); + $this->createError("Mailing is not configured on this server yet."); return null; } diff --git a/Core/API/TfaAPI.class.php b/Core/API/TfaAPI.class.php index 755b8e5..c297284 100644 --- a/Core/API/TfaAPI.class.php +++ b/Core/API/TfaAPI.class.php @@ -252,13 +252,18 @@ namespace Core\API\TFA { // $domain = "localhost"; if (!$clientDataJSON || !$attestationObjectRaw) { + $challenge = null; if ($twoFactorToken) { - if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) { + if ($twoFactorToken->isConfirmed()) { return $this->createError("You already added a two factor token"); - } else { + } else if ($twoFactorToken instanceof KeyBasedTwoFactorToken) { $challenge = $twoFactorToken->getChallenge(); + } else { + $twoFactorToken->delete($sql); } - } else { + } + + if ($challenge === null) { $twoFactorToken = KeyBasedTwoFactorToken::create(); $challenge = $twoFactorToken->getChallenge(); $this->success = ($twoFactorToken->save($sql) !== false); @@ -307,6 +312,10 @@ namespace Core\API\TFA { $this->success = $twoFactorToken->confirmKeyBased($sql, base64_encode($authData->getCredentialID()), $publicKey) !== false; $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize(); + } } return $this->success; diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php index d0cb6d2..e2d2ea8 100644 --- a/Core/API/UserAPI.class.php +++ b/Core/API/UserAPI.class.php @@ -1223,6 +1223,8 @@ namespace Core\API\User { $gpgKey = $currentUser->getGPG(); if ($gpgKey) { return $this->createError("You already added a GPG key to your account."); + } else if (!$currentUser->getEmail()) { + return $this->createError("You do not have an e-mail address"); } // fix key first, enforce a newline after @@ -1280,7 +1282,7 @@ namespace Core\API\User { if ($this->success) { $currentUser->gpgKey = $gpgKey; if ($currentUser->save($sql, ["gpgKey"])) { - $this->result["gpg"] = $gpgKey->jsonSerialize(); + $this->result["gpgKey"] = $gpgKey->jsonSerialize(); } else { return $this->createError("Error updating user details: " . $sql->getLastError()); } diff --git a/Core/Driver/SQL/SQL.class.php b/Core/Driver/SQL/SQL.class.php index 2357e02..1e9bc32 100644 --- a/Core/Driver/SQL/SQL.class.php +++ b/Core/Driver/SQL/SQL.class.php @@ -149,7 +149,7 @@ abstract class SQL { return false; } - $logLevel = Logger::LOG_LEVEL_DEBUG; + $logLevel = Logger::LOG_LEVEL_ERROR; if ($query instanceof Insert && $query->getTableName() === "SystemLog") { $logLevel = Logger::LOG_LEVEL_NONE; } diff --git a/Core/Localization/de_DE/account.php b/Core/Localization/de_DE/account.php index bd1980d..238eb6c 100644 --- a/Core/Localization/de_DE/account.php +++ b/Core/Localization/de_DE/account.php @@ -44,4 +44,5 @@ return [ "confirm_error" => "Fehler beim Bestätigen der E-Mail Adresse", "gpg_key" => "GPG-Schlüssel", "2fa_token" => "Zwei-Faktor Authentifizierung (2FA)", + "profile_picture_of" => "Profilbild von", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/account.php b/Core/Localization/en_US/account.php index 95a4c35..bf6ae1f 100644 --- a/Core/Localization/en_US/account.php +++ b/Core/Localization/en_US/account.php @@ -44,4 +44,5 @@ return [ "confirm_error" => "Error confirming e-mail address", "gpg_key" => "GPG Key", "2fa_token" => "Two-Factor Authentication (2FA)", + "profile_picture_of" => "Profile Picture of", ]; \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/ApiKey.class.php b/Core/Objects/DatabaseEntity/ApiKey.class.php index c3b3840..5544e7f 100644 --- a/Core/Objects/DatabaseEntity/ApiKey.class.php +++ b/Core/Objects/DatabaseEntity/ApiKey.class.php @@ -9,19 +9,23 @@ use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class ApiKey extends DatabaseEntity { private bool $active; - #[MaxLength(64)] public String $apiKey; + #[MaxLength(64)] public String $token; public \DateTime $validUntil; public User $user; - public function __construct(?int $id = null) { - parent::__construct($id); - $this->active = true; - } - public function getValidUntil(): \DateTime { return $this->validUntil; } + public static function create(User $user, int $days = 30): ApiKey { + $apiKey = new ApiKey(); + $apiKey->user = $user; + $apiKey->token = generateRandomString(64); + $apiKey->validUntil = (new \DateTime())->modify("+$days days"); + $apiKey->active = true; + return $apiKey; + } + public function refresh(SQL $sql, int $days): bool { $this->validUntil = (new \DateTime())->modify("+$days days"); return $this->save($sql, ["validUntil"]); diff --git a/Core/core.php b/Core/core.php index e3ccf1f..ef7e264 100644 --- a/Core/core.php +++ b/Core/core.php @@ -43,7 +43,7 @@ function uuidv4(): string { return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } -function generateRandomString($length, $type = "ascii"): string { +function generateRandomString(int $length, $type = "ascii"): string { $randomString = ''; $lowercase = "abcdefghijklmnopqrstuvwxyz"; diff --git a/react/shared/api.js b/react/shared/api.js index 685eb2e..193b900 100644 --- a/react/shared/api.js +++ b/react/shared/api.js @@ -156,8 +156,41 @@ export default class API { } async updateProfile(username=null, fullName=null, password=null, confirmPassword = null, oldPassword = null) { - return this.apiCall("user/updateProfile", { username: username, fullName: fullName, + let res = await this.apiCall("user/updateProfile", { username: username, fullName: fullName, password: password, confirmPassword: confirmPassword, oldPassword: oldPassword }); + + if (res.success) { + if (username !== null) { + this.user.name = username; + } + + if (fullName !== null) { + this.user.fullName = fullName; + } + } + + return res; + } + + async uploadPicture(file, scale=1.0) { + const formData = new FormData(); + formData.append("scale", scale); + formData.append("picture", file, file.name); + let res = await this.apiCall("user/uploadPicture", formData); + if (res.success) { + this.user.profilePicture = res.profilePicture; + } + + return res; + } + + async removePicture() { + let res = await this.apiCall("user/removePicture"); + if (res.success) { + this.user.profilePicture = null; + } + + return res; } /** Stats **/ @@ -234,8 +267,8 @@ export default class API { } /** ApiKeyAPI **/ - async getApiKeys(showActiveOnly = false) { - return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly }); + async getApiKeys(showActiveOnly = false, page = 1, count = 25, orderBy = "validUntil", sortOrder = "desc") { + return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly, pageNum: page, count: count, orderBy: orderBy, sortOrder: sortOrder }); } async createApiKey() { @@ -248,11 +281,21 @@ export default class API { /** 2FA API **/ async confirmTOTP(code) { - return this.apiCall("tfa/confirmTotp", { code: code }); + let res = await this.apiCall("tfa/confirmTotp", { code: code }); + if (res.success) { + this.user.twoFactorToken = { type: "totp", confirmed: true }; + } + + return res; } async remove2FA(password) { - return this.apiCall("tfa/remove", { password: password }); + let res = await this.apiCall("tfa/remove", { password: password }); + if (res.success) { + this.user.twoFactorToken = null; + } + + return res; } async verifyTotp2FA(code) { @@ -264,12 +307,22 @@ export default class API { } async register2FA(clientDataJSON = null, attestationObject = null) { - return this.apiCall("tfa/registerKey", { clientDataJSON: clientDataJSON, attestationObject: attestationObject }); + let res = await this.apiCall("tfa/registerKey", { clientDataJSON: clientDataJSON, attestationObject: attestationObject }); + if (res.success && res.twoFactorToken) { + this.user.twoFactorToken = res.twoFactorToken; + } + + return res; } /** GPG API **/ async uploadGPG(pubkey) { - return this.apiCall("user/importGPG", { pubkey: pubkey }); + let res = await this.apiCall("user/importGPG", { pubkey: pubkey }); + if (res.success) { + this.user.gpgKey = res.gpgKey; + } + + return res; } async confirmGpgToken(token) { diff --git a/react/shared/elements/data-table.js b/react/shared/elements/data-table.js index bc97a35..df476dd 100644 --- a/react/shared/elements/data-table.js +++ b/react/shared/elements/data-table.js @@ -7,7 +7,6 @@ import {LocaleContext} from "../locale"; import clsx from "clsx"; import {Box, IconButton} from "@mui/material"; import {formatDateTime} from "../util"; -import UserLink from "security-lab/src/elements/user/userlink"; import CachedIcon from "@material-ui/icons/Cached"; @@ -137,6 +136,7 @@ export class DataColumn { this.field = field; this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable; this.align = params.align || "left"; + this.params = params; } renderData(L, entry, index) { @@ -152,6 +152,16 @@ export class StringColumn extends DataColumn { constructor(label, field = null, params = {}) { super(label, field, params); } + + renderData(L, entry, index) { + let data = super.renderData(L, entry, index); + + if (this.params.style) { + data = {data} + } + + return data; + } } export class NumericColumn extends DataColumn { @@ -198,13 +208,14 @@ export class DateTimeColumn extends DataColumn { } } -export class UserLinkColumn extends DataColumn { +export class BoolColumn extends DataColumn { constructor(label, field = null, params = {}) { super(label, field, params); } renderData(L, entry, index) { - return + let data = super.renderData(L, entry); + return L(data ? "general.true" : "general.false"); } } diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index c5d37c2..6fe48fa 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -1,7 +1,7 @@ -import React from "react"; -import clsx from "clsx"; -import {Box, Modal} from "@mui/material"; -import {Button, Typography} from "@material-ui/core"; +import React, {useContext} from "react"; +import {Dialog as MuiDialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; +import {Button} from "@material-ui/core"; +import {LocaleContext} from "../locale"; import "./dialog.css"; export default function Dialog(props) { @@ -11,6 +11,8 @@ export default function Dialog(props) { const onOption = props.onOption || function() { }; const options = props.options || ["Close"]; const type = props.type || "default"; + const {translate: L} = useContext(LocaleContext); + let buttons = []; for (let name of options) { @@ -26,20 +28,19 @@ export default function Dialog(props) { ) } - return - - - {props.title} - - - {props.message} - - { buttons } - - + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description"> + { props.title } + + + { props.message } + + + + {buttons} + + } \ No newline at end of file diff --git a/react/shared/views/login.jsx b/react/shared/views/login.jsx index 91f9a53..96b0888 100644 --- a/react/shared/views/login.jsx +++ b/react/shared/views/login.jsx @@ -185,7 +185,7 @@ export default function LoginForm(props) { }).catch(e => { set2FAToken({ ...tfaToken, step: 2, error: e.toString() }); }); - }, [api.loggedIn, tfaToken, props.onLogin, abortSignal]); + }, [api.loggedIn, tfaToken, props.onLogin, props.onKey2FA, abortSignal]); const createForm = () => { @@ -335,7 +335,10 @@ export default function LoginForm(props) { } if (!loaded) { - return {L("general.loading")}… + return +

{L("general.loading", "Loading")}…

+ +
} let successMessage = getParameter("success");