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");