small changes
This commit is contained in:
parent
327f570316
commit
136ad48a5e
@ -16,6 +16,7 @@ namespace Core\API\ApiKey {
|
|||||||
|
|
||||||
use Core\API\ApiKeyAPI;
|
use Core\API\ApiKeyAPI;
|
||||||
use Core\API\Parameter\Parameter;
|
use Core\API\Parameter\Parameter;
|
||||||
|
use Core\API\Traits\Pagination;
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
use Core\Driver\SQL\Condition\CondAnd;
|
use Core\Driver\SQL\Condition\CondAnd;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
@ -32,17 +33,16 @@ namespace Core\API\ApiKey {
|
|||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
|
||||||
$apiKey = new ApiKey();
|
$apiKey = ApiKey::create($currentUser);
|
||||||
$apiKey->apiKey = generateRandomString(64);
|
|
||||||
$apiKey->validUntil = (new \DateTime())->modify("+30 DAY");
|
|
||||||
$apiKey->user = $this->context->getUser();
|
|
||||||
|
|
||||||
$this->success = $apiKey->save($sql);
|
$this->success = $apiKey->save($sql);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$this->result["api_key"] = $apiKey->jsonSerialize();
|
$this->result["apiKey"] = $apiKey->jsonSerialize(
|
||||||
|
["id", "validUntil", "token", "active"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
@ -55,10 +55,13 @@ namespace Core\API\ApiKey {
|
|||||||
|
|
||||||
class Fetch extends ApiKeyAPI {
|
class Fetch extends ApiKeyAPI {
|
||||||
|
|
||||||
|
use Pagination;
|
||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
$params = $this->getPaginationParameters(["token", "validUntil", "active"]);
|
||||||
"showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true)
|
$params["showActiveOnly"] = new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true);
|
||||||
));
|
|
||||||
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->loginRequired = true;
|
$this->loginRequired = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,14 +77,18 @@ namespace Core\API\ApiKey {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiKeys = ApiKey::findAll($sql, $condition);
|
if (!$this->initPagination($sql, ApiKey::class, $condition)) {
|
||||||
$this->success = ($apiKeys !== FALSE);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKeys = $this->createPaginationQuery($sql)->execute();
|
||||||
|
$this->success = ($apiKeys !== FALSE && $apiKeys !== null);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$this->result["api_keys"] = array();
|
$this->result["apiKeys"] = [];
|
||||||
foreach($apiKeys as $apiKey) {
|
foreach($apiKeys as $apiKey) {
|
||||||
$this->result["api_keys"][$apiKey->getId()] = $apiKey->jsonSerialize();
|
$this->result["apiKeys"][] = $apiKey->jsonSerialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ namespace Core\API {
|
|||||||
$settings = $req->getResult()["settings"];
|
$settings = $req->getResult()["settings"];
|
||||||
|
|
||||||
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,13 +252,18 @@ namespace Core\API\TFA {
|
|||||||
// $domain = "localhost";
|
// $domain = "localhost";
|
||||||
|
|
||||||
if (!$clientDataJSON || !$attestationObjectRaw) {
|
if (!$clientDataJSON || !$attestationObjectRaw) {
|
||||||
|
$challenge = null;
|
||||||
if ($twoFactorToken) {
|
if ($twoFactorToken) {
|
||||||
if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) {
|
if ($twoFactorToken->isConfirmed()) {
|
||||||
return $this->createError("You already added a two factor token");
|
return $this->createError("You already added a two factor token");
|
||||||
} else {
|
} else if ($twoFactorToken instanceof KeyBasedTwoFactorToken) {
|
||||||
$challenge = $twoFactorToken->getChallenge();
|
$challenge = $twoFactorToken->getChallenge();
|
||||||
|
} else {
|
||||||
|
$twoFactorToken->delete($sql);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if ($challenge === null) {
|
||||||
$twoFactorToken = KeyBasedTwoFactorToken::create();
|
$twoFactorToken = KeyBasedTwoFactorToken::create();
|
||||||
$challenge = $twoFactorToken->getChallenge();
|
$challenge = $twoFactorToken->getChallenge();
|
||||||
$this->success = ($twoFactorToken->save($sql) !== false);
|
$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->success = $twoFactorToken->confirmKeyBased($sql, base64_encode($authData->getCredentialID()), $publicKey) !== false;
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
$this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
|
@ -1223,6 +1223,8 @@ namespace Core\API\User {
|
|||||||
$gpgKey = $currentUser->getGPG();
|
$gpgKey = $currentUser->getGPG();
|
||||||
if ($gpgKey) {
|
if ($gpgKey) {
|
||||||
return $this->createError("You already added a GPG key to your account.");
|
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
|
// fix key first, enforce a newline after
|
||||||
@ -1280,7 +1282,7 @@ namespace Core\API\User {
|
|||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$currentUser->gpgKey = $gpgKey;
|
$currentUser->gpgKey = $gpgKey;
|
||||||
if ($currentUser->save($sql, ["gpgKey"])) {
|
if ($currentUser->save($sql, ["gpgKey"])) {
|
||||||
$this->result["gpg"] = $gpgKey->jsonSerialize();
|
$this->result["gpgKey"] = $gpgKey->jsonSerialize();
|
||||||
} else {
|
} else {
|
||||||
return $this->createError("Error updating user details: " . $sql->getLastError());
|
return $this->createError("Error updating user details: " . $sql->getLastError());
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ abstract class SQL {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$logLevel = Logger::LOG_LEVEL_DEBUG;
|
$logLevel = Logger::LOG_LEVEL_ERROR;
|
||||||
if ($query instanceof Insert && $query->getTableName() === "SystemLog") {
|
if ($query instanceof Insert && $query->getTableName() === "SystemLog") {
|
||||||
$logLevel = Logger::LOG_LEVEL_NONE;
|
$logLevel = Logger::LOG_LEVEL_NONE;
|
||||||
}
|
}
|
||||||
|
@ -44,4 +44,5 @@ return [
|
|||||||
"confirm_error" => "Fehler beim Bestätigen der E-Mail Adresse",
|
"confirm_error" => "Fehler beim Bestätigen der E-Mail Adresse",
|
||||||
"gpg_key" => "GPG-Schlüssel",
|
"gpg_key" => "GPG-Schlüssel",
|
||||||
"2fa_token" => "Zwei-Faktor Authentifizierung (2FA)",
|
"2fa_token" => "Zwei-Faktor Authentifizierung (2FA)",
|
||||||
|
"profile_picture_of" => "Profilbild von",
|
||||||
];
|
];
|
@ -44,4 +44,5 @@ return [
|
|||||||
"confirm_error" => "Error confirming e-mail address",
|
"confirm_error" => "Error confirming e-mail address",
|
||||||
"gpg_key" => "GPG Key",
|
"gpg_key" => "GPG Key",
|
||||||
"2fa_token" => "Two-Factor Authentication (2FA)",
|
"2fa_token" => "Two-Factor Authentication (2FA)",
|
||||||
|
"profile_picture_of" => "Profile Picture of",
|
||||||
];
|
];
|
@ -9,19 +9,23 @@ use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
|||||||
class ApiKey extends DatabaseEntity {
|
class ApiKey extends DatabaseEntity {
|
||||||
|
|
||||||
private bool $active;
|
private bool $active;
|
||||||
#[MaxLength(64)] public String $apiKey;
|
#[MaxLength(64)] public String $token;
|
||||||
public \DateTime $validUntil;
|
public \DateTime $validUntil;
|
||||||
public User $user;
|
public User $user;
|
||||||
|
|
||||||
public function __construct(?int $id = null) {
|
|
||||||
parent::__construct($id);
|
|
||||||
$this->active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getValidUntil(): \DateTime {
|
public function getValidUntil(): \DateTime {
|
||||||
return $this->validUntil;
|
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 {
|
public function refresh(SQL $sql, int $days): bool {
|
||||||
$this->validUntil = (new \DateTime())->modify("+$days days");
|
$this->validUntil = (new \DateTime())->modify("+$days days");
|
||||||
return $this->save($sql, ["validUntil"]);
|
return $this->save($sql, ["validUntil"]);
|
||||||
|
@ -43,7 +43,7 @@ function uuidv4(): string {
|
|||||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
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 = '';
|
$randomString = '';
|
||||||
|
|
||||||
$lowercase = "abcdefghijklmnopqrstuvwxyz";
|
$lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
@ -156,8 +156,41 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateProfile(username=null, fullName=null, password=null, confirmPassword = null, oldPassword = null) {
|
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 });
|
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 **/
|
/** Stats **/
|
||||||
@ -234,8 +267,8 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** ApiKeyAPI **/
|
/** ApiKeyAPI **/
|
||||||
async getApiKeys(showActiveOnly = false) {
|
async getApiKeys(showActiveOnly = false, page = 1, count = 25, orderBy = "validUntil", sortOrder = "desc") {
|
||||||
return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly });
|
return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly, pageNum: page, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApiKey() {
|
async createApiKey() {
|
||||||
@ -248,11 +281,21 @@ export default class API {
|
|||||||
|
|
||||||
/** 2FA API **/
|
/** 2FA API **/
|
||||||
async confirmTOTP(code) {
|
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) {
|
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) {
|
async verifyTotp2FA(code) {
|
||||||
@ -264,12 +307,22 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async register2FA(clientDataJSON = null, attestationObject = null) {
|
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 **/
|
/** GPG API **/
|
||||||
async uploadGPG(pubkey) {
|
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) {
|
async confirmGpgToken(token) {
|
||||||
|
@ -7,7 +7,6 @@ import {LocaleContext} from "../locale";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Box, IconButton} from "@mui/material";
|
import {Box, IconButton} from "@mui/material";
|
||||||
import {formatDateTime} from "../util";
|
import {formatDateTime} from "../util";
|
||||||
import UserLink from "security-lab/src/elements/user/userlink";
|
|
||||||
import CachedIcon from "@material-ui/icons/Cached";
|
import CachedIcon from "@material-ui/icons/Cached";
|
||||||
|
|
||||||
|
|
||||||
@ -137,6 +136,7 @@ export class DataColumn {
|
|||||||
this.field = field;
|
this.field = field;
|
||||||
this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable;
|
this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable;
|
||||||
this.align = params.align || "left";
|
this.align = params.align || "left";
|
||||||
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
@ -152,6 +152,16 @@ export class StringColumn extends DataColumn {
|
|||||||
constructor(label, field = null, params = {}) {
|
constructor(label, field = null, params = {}) {
|
||||||
super(label, field, params);
|
super(label, field, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderData(L, entry, index) {
|
||||||
|
let data = super.renderData(L, entry, index);
|
||||||
|
|
||||||
|
if (this.params.style) {
|
||||||
|
data = <span style={this.params.style}>{data}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumericColumn extends DataColumn {
|
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 = {}) {
|
constructor(label, field = null, params = {}) {
|
||||||
super(label, field, params);
|
super(label, field, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
return <UserLink user={super.renderData(L, entry)}/>
|
let data = super.renderData(L, entry);
|
||||||
|
return L(data ? "general.true" : "general.false");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React, {useContext} from "react";
|
||||||
import clsx from "clsx";
|
import {Dialog as MuiDialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material";
|
||||||
import {Box, Modal} from "@mui/material";
|
import {Button} from "@material-ui/core";
|
||||||
import {Button, Typography} from "@material-ui/core";
|
import {LocaleContext} from "../locale";
|
||||||
import "./dialog.css";
|
import "./dialog.css";
|
||||||
|
|
||||||
export default function Dialog(props) {
|
export default function Dialog(props) {
|
||||||
@ -11,6 +11,8 @@ export default function Dialog(props) {
|
|||||||
const onOption = props.onOption || function() { };
|
const onOption = props.onOption || function() { };
|
||||||
const options = props.options || ["Close"];
|
const options = props.options || ["Close"];
|
||||||
const type = props.type || "default";
|
const type = props.type || "default";
|
||||||
|
const {translate: L} = useContext(LocaleContext);
|
||||||
|
|
||||||
|
|
||||||
let buttons = [];
|
let buttons = [];
|
||||||
for (let name of options) {
|
for (let name of options) {
|
||||||
@ -26,20 +28,19 @@ export default function Dialog(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Modal
|
return <MuiDialog
|
||||||
open={show}
|
open={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
aria-labelledby="modal-title"
|
aria-labelledby="alert-dialog-title"
|
||||||
aria-describedby="modal-description"
|
aria-describedby="alert-dialog-description">
|
||||||
>
|
<DialogTitle>{ props.title }</DialogTitle>
|
||||||
<Box className={clsx("modal-dialog", props.className)}>
|
<DialogContent>
|
||||||
<Typography id="modal-title" variant="h6" component="h2">
|
<DialogContentText>
|
||||||
{props.title}
|
{ props.message }
|
||||||
</Typography>
|
</DialogContentText>
|
||||||
<Typography id="modal-description" sx={{ mt: 2 }}>
|
</DialogContent>
|
||||||
{props.message}
|
<DialogActions>
|
||||||
</Typography>
|
{buttons}
|
||||||
{ buttons }
|
</DialogActions>
|
||||||
</Box>
|
</MuiDialog>
|
||||||
</Modal>
|
|
||||||
}
|
}
|
@ -185,7 +185,7 @@ export default function LoginForm(props) {
|
|||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
set2FAToken({ ...tfaToken, step: 2, error: e.toString() });
|
set2FAToken({ ...tfaToken, step: 2, error: e.toString() });
|
||||||
});
|
});
|
||||||
}, [api.loggedIn, tfaToken, props.onLogin, abortSignal]);
|
}, [api.loggedIn, tfaToken, props.onLogin, props.onKey2FA, abortSignal]);
|
||||||
|
|
||||||
const createForm = () => {
|
const createForm = () => {
|
||||||
|
|
||||||
@ -335,7 +335,10 @@ export default function LoginForm(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return <b>{L("general.loading")}… <Icon icon={"spinner"}/></b>
|
return <Box textAlign={"center"} mt={2}>
|
||||||
|
<h2>{L("general.loading", "Loading")}…</h2>
|
||||||
|
<CircularProgress size={"32px"}/>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
let successMessage = getParameter("success");
|
let successMessage = getParameter("success");
|
||||||
|
Loading…
Reference in New Issue
Block a user