Mail bugfix, gpg, profile frontend WIP
This commit is contained in:
parent
fe81e0f6fa
commit
e97ac34365
@ -55,14 +55,15 @@ namespace Core\API\Mail {
|
|||||||
use Core\External\PHPMailer\PHPMailer;
|
use Core\External\PHPMailer\PHPMailer;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\GpgKey;
|
use Core\Objects\DatabaseEntity\GpgKey;
|
||||||
|
use PhpParser\Node\Param;
|
||||||
|
|
||||||
class Test extends MailAPI {
|
class Test extends MailAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL),
|
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL),
|
||||||
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null)
|
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null)
|
||||||
));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -74,10 +75,12 @@ namespace Core\API\Mail {
|
|||||||
"subject" => "Test E-Mail",
|
"subject" => "Test E-Mail",
|
||||||
"body" => "Hey! If you receive this e-mail, your mail configuration seems to be working.",
|
"body" => "Hey! If you receive this e-mail, your mail configuration seems to be working.",
|
||||||
"gpgFingerprint" => $this->getParam("gpgFingerprint"),
|
"gpgFingerprint" => $this->getParam("gpgFingerprint"),
|
||||||
"async" => false
|
"async" => false,
|
||||||
|
"debug" => true,
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->lastError = $req->getLastError();
|
$this->lastError = $req->getLastError();
|
||||||
|
$this->result["output"] = $req->getResult()["output"];
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +98,8 @@ namespace Core\API\Mail {
|
|||||||
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
|
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
|
||||||
'replyName' => new StringType('replyName', 32, true, ""),
|
'replyName' => new StringType('replyName', 32, true, ""),
|
||||||
'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
|
'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
|
||||||
'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, null)
|
'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, null),
|
||||||
|
'debug' => new Parameter("debug", Parameter::TYPE_BOOLEAN, true, false)
|
||||||
));
|
));
|
||||||
$this->isPublic = false;
|
$this->isPublic = false;
|
||||||
}
|
}
|
||||||
@ -115,6 +119,7 @@ namespace Core\API\Mail {
|
|||||||
$replyName = $this->getParam('replyName');
|
$replyName = $this->getParam('replyName');
|
||||||
$body = $this->getParam('body');
|
$body = $this->getParam('body');
|
||||||
$gpgFingerprint = $this->getParam("gpgFingerprint");
|
$gpgFingerprint = $this->getParam("gpgFingerprint");
|
||||||
|
$debug = $this->getParam("debug");
|
||||||
|
|
||||||
$mailAsync = $this->getParam("async");
|
$mailAsync = $this->getParam("async");
|
||||||
if ($mailAsync === null) {
|
if ($mailAsync === null) {
|
||||||
@ -156,8 +161,9 @@ namespace Core\API\Mail {
|
|||||||
$mail->addReplyTo($replyTo, $replyName);
|
$mail->addReplyTo($replyTo, $replyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$mail->Subject = $subject;
|
$mail->Subject = $subject;
|
||||||
$mail->SMTPDebug = 0;
|
$mail->SMTPDebug = $debug ? 2 : 0;
|
||||||
$mail->Host = $mailConfig->getHost();
|
$mail->Host = $mailConfig->getHost();
|
||||||
$mail->Port = $mailConfig->getPort();
|
$mail->Port = $mailConfig->getPort();
|
||||||
$mail->SMTPAuth = true;
|
$mail->SMTPAuth = true;
|
||||||
@ -193,12 +199,22 @@ namespace Core\API\Mail {
|
|||||||
$mail->AltBody = strip_tags($body);
|
$mail->AltBody = strip_tags($body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
$this->success = @$mail->Send();
|
$this->success = @$mail->Send();
|
||||||
|
$output = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
if (!$this->success) {
|
if (!$this->success) {
|
||||||
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
|
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
|
||||||
$this->logger->error("sendMail() failed: $mail->ErrorInfo");
|
$this->logger->error("sendMail() failed: $mail->ErrorInfo");
|
||||||
|
if ($debug) {
|
||||||
|
$this->logger->debug($output);
|
||||||
|
$this->result["output"] = $output;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->result["messageId"] = $mail->getLastMessageID();
|
$this->result["messageId"] = $mail->getLastMessageID();
|
||||||
|
if ($debug) {
|
||||||
|
$this->result["output"] = $output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->success = false;
|
$this->success = false;
|
||||||
|
@ -1240,9 +1240,9 @@ namespace Core\API\User {
|
|||||||
class ImportGPG extends UserAPI {
|
class ImportGPG extends UserAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
"pubkey" => new StringType("pubkey")
|
"pubkey" => new StringType("pubkey")
|
||||||
));
|
]);
|
||||||
$this->loginRequired = true;
|
$this->loginRequired = true;
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
}
|
}
|
||||||
@ -1342,6 +1342,10 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), [], "Allows users to import gpg keys for a secure e-mail communication", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoveGPG extends UserAPI {
|
class RemoveGPG extends UserAPI {
|
||||||
@ -1371,6 +1375,10 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), [], "Allows users to unlink gpg keys from their profile", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfirmGPG extends UserAPI {
|
class ConfirmGPG extends UserAPI {
|
||||||
|
@ -85,4 +85,15 @@ return [
|
|||||||
"remove_group_member_text" => "Möchten Sie wirklich den Benutzer '%s' von dieser Gruppe entfernen?",
|
"remove_group_member_text" => "Möchten Sie wirklich den Benutzer '%s' von dieser Gruppe entfernen?",
|
||||||
"add_group_member_title" => "Mitglied hinzufügen",
|
"add_group_member_title" => "Mitglied hinzufügen",
|
||||||
"add_group_member_text" => "Einen Benutzer suchen um ihn der Gruppe hinzuzufügen",
|
"add_group_member_text" => "Einen Benutzer suchen um ihn der Gruppe hinzuzufügen",
|
||||||
|
|
||||||
|
# GPG Key
|
||||||
|
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
||||||
|
|
||||||
|
# 2fa
|
||||||
|
"register_2fa_device" => "Ein 2FA-Gerät registrieren",
|
||||||
|
"register_2fa_totp_text" => "Scan den QR-Code mit einem Gerät, das du als Zwei-Faktor-Authentifizierung (2FA) benutzen willst. " .
|
||||||
|
"Unter Android kannst du den Google Authenticator benutzen.",
|
||||||
|
"register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts",
|
||||||
|
"remove_2fa" => "2FA-Token entfernen",
|
||||||
|
"remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen",
|
||||||
];
|
];
|
@ -34,6 +34,7 @@ return [
|
|||||||
"yes" => "Ja",
|
"yes" => "Ja",
|
||||||
"no" => "Nein",
|
"no" => "Nein",
|
||||||
"create_new" => "Erstellen",
|
"create_new" => "Erstellen",
|
||||||
|
"unchanged" => "Unverändert",
|
||||||
|
|
||||||
# dialog / actions
|
# dialog / actions
|
||||||
"action" => "Aktion",
|
"action" => "Aktion",
|
||||||
|
@ -12,17 +12,6 @@ return [
|
|||||||
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
||||||
"no_api_key_registered" => "Keine gültigen API-Schlüssel registriert",
|
"no_api_key_registered" => "Keine gültigen API-Schlüssel registriert",
|
||||||
|
|
||||||
# GPG Key
|
|
||||||
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
|
||||||
|
|
||||||
# 2fa
|
|
||||||
"register_2fa_device" => "Ein 2FA-Gerät registrieren",
|
|
||||||
"register_2fa_totp_text" => "Scan den QR-Code mit einem Gerät, das du als Zwei-Faktor-Authentifizierung (2FA) benutzen willst. " .
|
|
||||||
"Unter Android kannst du den Google Authenticator benutzen.",
|
|
||||||
"register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts",
|
|
||||||
"remove_2fa" => "2FA-Token entfernen",
|
|
||||||
"remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen",
|
|
||||||
|
|
||||||
# settings
|
# settings
|
||||||
"key" => "Schlüssel",
|
"key" => "Schlüssel",
|
||||||
"value" => "Wert",
|
"value" => "Wert",
|
||||||
@ -30,7 +19,6 @@ return [
|
|||||||
"mail" => "Mail",
|
"mail" => "Mail",
|
||||||
"recaptcha" => "reCaptcha",
|
"recaptcha" => "reCaptcha",
|
||||||
"uncategorized" => "Unkategorisiert",
|
"uncategorized" => "Unkategorisiert",
|
||||||
"unchanged" => "Unverändert",
|
|
||||||
|
|
||||||
# general settings
|
# general settings
|
||||||
"site_name" => "Seitenname",
|
"site_name" => "Seitenname",
|
||||||
|
@ -85,4 +85,15 @@ return [
|
|||||||
"remove_group_member_text" => "Do you really want to remove user '%s' from this group?",
|
"remove_group_member_text" => "Do you really want to remove user '%s' from this group?",
|
||||||
"add_group_member_title" => "Add member",
|
"add_group_member_title" => "Add member",
|
||||||
"add_group_member_text" => "Search a user to add to the group",
|
"add_group_member_text" => "Search a user to add to the group",
|
||||||
|
|
||||||
|
# GPG Key
|
||||||
|
"gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...",
|
||||||
|
|
||||||
|
# 2fa
|
||||||
|
"register_2fa_device" => "Register a 2FA-Device",
|
||||||
|
"register_2fa_totp_text" => "Scan the QR-Code with a device you want to use for Two-Factor-Authentication (2FA). " .
|
||||||
|
"On Android, you can use the Google Authenticator.",
|
||||||
|
"register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.",
|
||||||
|
"remove_2fa" => "Remove 2FA Token",
|
||||||
|
"remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token",
|
||||||
];
|
];
|
@ -16,6 +16,7 @@ return [
|
|||||||
"yes" => "Yes",
|
"yes" => "Yes",
|
||||||
"no" => "No",
|
"no" => "No",
|
||||||
"create_new" => "Create",
|
"create_new" => "Create",
|
||||||
|
"unchanged" => "Unchanged",
|
||||||
|
|
||||||
# dialog / actions
|
# dialog / actions
|
||||||
"action" => "Action",
|
"action" => "Action",
|
||||||
|
@ -12,17 +12,6 @@ return [
|
|||||||
"show_only_active_keys" => "Show only active keys",
|
"show_only_active_keys" => "Show only active keys",
|
||||||
"no_api_key_registered" => "No valid API-Keys registered",
|
"no_api_key_registered" => "No valid API-Keys registered",
|
||||||
|
|
||||||
# GPG Key
|
|
||||||
"gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...",
|
|
||||||
|
|
||||||
# 2fa
|
|
||||||
"register_2fa_device" => "Register a 2FA-Device",
|
|
||||||
"register_2fa_totp_text" => "Scan the QR-Code with a device you want to use for Two-Factor-Authentication (2FA). " .
|
|
||||||
"On Android, you can use the Google Authenticator.",
|
|
||||||
"register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.",
|
|
||||||
"remove_2fa" => "Remove 2FA Token",
|
|
||||||
"remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token",
|
|
||||||
|
|
||||||
# settings
|
# settings
|
||||||
"key" => "Key",
|
"key" => "Key",
|
||||||
"value" => "Value",
|
"value" => "Value",
|
||||||
@ -30,7 +19,6 @@ return [
|
|||||||
"mail" => "Mail",
|
"mail" => "Mail",
|
||||||
"recaptcha" => "reCaptcha",
|
"recaptcha" => "reCaptcha",
|
||||||
"uncategorized" => "Uncategorized",
|
"uncategorized" => "Uncategorized",
|
||||||
"unchanged" => "Unchanged",
|
|
||||||
|
|
||||||
# general settings
|
# general settings
|
||||||
"site_name" => "Site Name",
|
"site_name" => "Site Name",
|
||||||
|
@ -16,7 +16,6 @@ Web-Base is a php framework which provides basic web functionalities and a moder
|
|||||||
- [Command Line Interface (CLI)](#cli)
|
- [Command Line Interface (CLI)](#cli)
|
||||||
- [Account & User functions](#access-control)
|
- [Account & User functions](#access-control)
|
||||||
- Admin Dashboard
|
- Admin Dashboard
|
||||||
- File Sharing Dashboard
|
|
||||||
- Docker Support
|
- Docker Support
|
||||||
|
|
||||||
### Upcoming:
|
### Upcoming:
|
||||||
|
30
cli.php
30
cli.php
@ -630,8 +630,33 @@ function onMail($argv): void {
|
|||||||
if (!$req->execute(["debug" => $debug])) {
|
if (!$req->execute(["debug" => $debug])) {
|
||||||
_exit("Error processing mail queue: " . $req->getLastError());
|
_exit("Error processing mail queue: " . $req->getLastError());
|
||||||
}
|
}
|
||||||
|
} else if ($action === "test") {
|
||||||
|
$recipient = $argv[3] ?? null;
|
||||||
|
$gpgFingerprint = $argv[4] ?? null;
|
||||||
|
if (!$recipient) {
|
||||||
|
_exit("Usage: cli.php mail test <recipient> [gpg-fingerprint]");
|
||||||
|
}
|
||||||
|
|
||||||
|
connectSQL() or die();
|
||||||
|
$req = new \Core\API\Mail\Test($context);
|
||||||
|
$success = $req->execute([
|
||||||
|
"receiver" => $recipient,
|
||||||
|
"gpgFingerprint" => $gpgFingerprint,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $req->getResult();
|
||||||
|
if ($success) {
|
||||||
|
printLine("Test email sent successfully");
|
||||||
|
} else {
|
||||||
|
printLine("Test email failed to sent: " . $req->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists("output", $result)) {
|
||||||
|
printLine();
|
||||||
|
printLine($result["output"]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_exit("Usage: cli.php mail <send_queue> [options...]");
|
_exit("Usage: cli.php mail <send_queue|test> [options...]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,7 +668,6 @@ function onImpersonate($argv): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sql = connectSQL() or die();
|
$sql = connectSQL() or die();
|
||||||
|
|
||||||
$userId = $argv[2];
|
$userId = $argv[2];
|
||||||
if (!is_numeric($userId)) {
|
if (!is_numeric($userId)) {
|
||||||
$res = $sql->select("id")
|
$res = $sql->select("id")
|
||||||
@ -932,7 +956,7 @@ $registeredCommands = [
|
|||||||
"routes" => ["handler" => "onRoutes", "description" => "view and modify routes"],
|
"routes" => ["handler" => "onRoutes", "description" => "view and modify routes"],
|
||||||
"maintenance" => ["handler" => "onMaintenance", "description" => "toggle maintenance mode"],
|
"maintenance" => ["handler" => "onMaintenance", "description" => "toggle maintenance mode"],
|
||||||
"test" => ["handler" => "onTest", "description" => "run unit and integration tests", "requiresDocker" => true],
|
"test" => ["handler" => "onTest", "description" => "run unit and integration tests", "requiresDocker" => true],
|
||||||
"mail" => ["handler" => "onMail", "description" => "send mails and process the pipeline"],
|
"mail" => ["handler" => "onMail", "description" => "send mails and process the pipeline", "requiresDocker" => true],
|
||||||
"settings" => ["handler" => "onSettings", "description" => "change and view settings"],
|
"settings" => ["handler" => "onSettings", "description" => "change and view settings"],
|
||||||
"impersonate" => ["handler" => "onImpersonate", "description" => "create a session and print cookies and csrf tokens", "requiresDocker" => true],
|
"impersonate" => ["handler" => "onImpersonate", "description" => "create a session and print cookies and csrf tokens", "requiresDocker" => true],
|
||||||
"frontend" => ["handler" => "onFrontend", "description" => "build and manage frontend modules"],
|
"frontend" => ["handler" => "onFrontend", "description" => "build and manage frontend modules"],
|
||||||
|
9
react/admin-panel/src/elements/button-bar.js
Normal file
9
react/admin-panel/src/elements/button-bar.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {Box, styled} from "@mui/material";
|
||||||
|
|
||||||
|
const ButtonBar = styled(Box)((props) => ({
|
||||||
|
"& > button, & > label": {
|
||||||
|
marginRight: props.theme.spacing(1)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default ButtonBar;
|
@ -7,9 +7,10 @@ import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/ele
|
|||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import usePagination from "shared/hooks/pagination";
|
import usePagination from "shared/hooks/pagination";
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
import {Box, FormControl, FormGroup, FormLabel, styled, TextField, Button, CircularProgress} from "@mui/material";
|
import {FormControl, FormGroup, FormLabel, styled, TextField, Button, CircularProgress} from "@mui/material";
|
||||||
import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material";
|
import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||||
import {MuiColorInput} from "mui-color-input";
|
import {MuiColorInput} from "mui-color-input";
|
||||||
|
import ButtonBar from "../../elements/button-bar";
|
||||||
|
|
||||||
const defaultGroupData = {
|
const defaultGroupData = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -17,12 +18,6 @@ const defaultGroupData = {
|
|||||||
members: []
|
members: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const ButtonBar = styled(Box)((props) => ({
|
|
||||||
"& > button": {
|
|
||||||
marginRight: props.theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function EditGroupView(props) {
|
export default function EditGroupView(props) {
|
||||||
|
|
||||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
|
39
react/admin-panel/src/views/profile/collapse-box.js
Normal file
39
react/admin-panel/src/views/profile/collapse-box.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Box, Collapse, FormControl, FormGroup, FormLabel, Paper, styled, TextField} from "@mui/material";
|
||||||
|
import {ExpandLess, ExpandMore} from "@mui/icons-material";
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)((props) => ({
|
||||||
|
"& > header": {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "50px 50px auto",
|
||||||
|
cursor: "pointer",
|
||||||
|
marginTop: props.theme.spacing(1),
|
||||||
|
padding: props.theme.spacing(1),
|
||||||
|
"& > svg": {
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& > h5": {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"& > div:nth-of-type(1)": {
|
||||||
|
padding: props.theme.spacing(2),
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: props.theme.palette.divider,
|
||||||
|
borderTopStyle: "solid"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function CollapseBox(props) {
|
||||||
|
const {open, title, icon, children, onToggle, ...other} = props;
|
||||||
|
|
||||||
|
return <StyledBox component={Paper} {...other}>
|
||||||
|
<header onClick={onToggle}>
|
||||||
|
{ open ? <ExpandLess/> : <ExpandMore /> }
|
||||||
|
{ icon }
|
||||||
|
<h5>{title}</h5>
|
||||||
|
</header>
|
||||||
|
<Collapse in={open} timeout={"auto"} unmountOnExit>
|
||||||
|
{children}
|
||||||
|
</Collapse>
|
||||||
|
</StyledBox>
|
||||||
|
}
|
@ -1,8 +1,62 @@
|
|||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {Button, CircularProgress, FormControl, FormGroup, FormLabel, TextField} from "@mui/material";
|
import {
|
||||||
import {Save} from "@mui/icons-material";
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormLabel, styled,
|
||||||
|
TextField
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
CloudUpload,
|
||||||
|
ErrorOutline,
|
||||||
|
Fingerprint,
|
||||||
|
Password,
|
||||||
|
Remove,
|
||||||
|
Save,
|
||||||
|
Upload,
|
||||||
|
VpnKey
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import CollapseBox from "./collapse-box";
|
||||||
|
import ButtonBar from "../../elements/button-bar";
|
||||||
|
|
||||||
|
const GpgKeyField = styled(TextField)((props) => ({
|
||||||
|
"& > div": {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
padding: props.theme.spacing(1),
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
},
|
||||||
|
marginBottom: props.theme.spacing(1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const GpgFingerprintBox = styled(Box)((props) => ({
|
||||||
|
"& > svg": {
|
||||||
|
marginRight: props.theme.spacing(1),
|
||||||
|
},
|
||||||
|
"& > code": {
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ProfileFormGroup = styled(FormGroup)((props) => ({
|
||||||
|
marginBottom: props.theme.spacing(2)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VisuallyHiddenInput = styled('input')({
|
||||||
|
clip: 'rect(0 0 0 0)',
|
||||||
|
clipPath: 'inset(50%)',
|
||||||
|
height: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 1,
|
||||||
|
});
|
||||||
|
|
||||||
export default function ProfileView(props) {
|
export default function ProfileView(props) {
|
||||||
|
|
||||||
@ -22,9 +76,14 @@ export default function ProfileView(props) {
|
|||||||
// data
|
// data
|
||||||
const [profile, setProfile] = useState({...api.user});
|
const [profile, setProfile] = useState({...api.user});
|
||||||
const [changePassword, setChangePassword] = useState({ old: "", new: "", confirm: "" });
|
const [changePassword, setChangePassword] = useState({ old: "", new: "", confirm: "" });
|
||||||
|
const [gpgKey, setGpgKey] = useState("");
|
||||||
|
const [gpgKeyPassword, setGpgKeyPassword] = useState("");
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
|
const [openedTab, setOpenedTab] = useState(null);
|
||||||
const [isSaving, setSaving] = useState(false);
|
const [isSaving, setSaving] = useState(false);
|
||||||
|
const [isGpgKeyUploading, setGpgKeyUploading] = useState(false);
|
||||||
|
const [isGpgKeyRemoving, setGpgKeyRemoving] = useState(false);
|
||||||
|
|
||||||
const onUpdateProfile = useCallback(() => {
|
const onUpdateProfile = useCallback(() => {
|
||||||
|
|
||||||
@ -60,6 +119,54 @@ export default function ProfileView(props) {
|
|||||||
|
|
||||||
}, [profile, changePassword, api, showDialog, isSaving]);
|
}, [profile, changePassword, api, showDialog, isSaving]);
|
||||||
|
|
||||||
|
const onUploadGPG = useCallback(() => {
|
||||||
|
if (!isGpgKeyUploading) {
|
||||||
|
setGpgKeyUploading(true);
|
||||||
|
api.uploadGPG(gpgKey).then(data => {
|
||||||
|
setGpgKeyUploading(false);
|
||||||
|
if (!data.success) {
|
||||||
|
showDialog(data.msg, L("account.upload_gpg_error"));
|
||||||
|
} else {
|
||||||
|
setProfile({...profile, gpgKey: data.gpgKey});
|
||||||
|
setGpgKey("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [api, showDialog, isGpgKeyUploading, profile, gpgKey]);
|
||||||
|
|
||||||
|
const onRemoveGpgKey = useCallback(() => {
|
||||||
|
if (!isGpgKeyRemoving) {
|
||||||
|
setGpgKeyRemoving(true);
|
||||||
|
api.removeGPG(gpgKeyPassword).then(data => {
|
||||||
|
setGpgKeyRemoving(false);
|
||||||
|
setGpgKeyPassword("");
|
||||||
|
if (!data.success) {
|
||||||
|
showDialog(data.msg, L("account.remove_gpg_error"));
|
||||||
|
} else {
|
||||||
|
setProfile({...profile, gpgKey: null});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [api, showDialog, isGpgKeyRemoving, gpgKeyPassword]);
|
||||||
|
|
||||||
|
const getFileContents = useCallback((file, callback) => {
|
||||||
|
let reader = new FileReader();
|
||||||
|
let data = "";
|
||||||
|
reader.onload = function(event) {
|
||||||
|
data += event.target.result;
|
||||||
|
if (reader.readyState === 2) {
|
||||||
|
if (!data.match(/^-+\s*BEGIN/m)) {
|
||||||
|
showDialog(L("Selected file is a not a GPG Public Key in ASCII format"), L("Error reading file"));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setGpgKey("");
|
||||||
|
reader.readAsText(file);
|
||||||
|
}, [showDialog]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"content-header"}>
|
<div className={"content-header"}>
|
||||||
<div className={"container-fluid"}>
|
<div className={"container-fluid"}>
|
||||||
@ -79,7 +186,7 @@ export default function ProfileView(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"content"}>
|
<div className={"content"}>
|
||||||
<FormGroup>
|
<ProfileFormGroup>
|
||||||
<FormLabel>{L("account.username")}</FormLabel>
|
<FormLabel>{L("account.username")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TextField variant={"outlined"}
|
<TextField variant={"outlined"}
|
||||||
@ -87,8 +194,8 @@ export default function ProfileView(props) {
|
|||||||
value={profile.name}
|
value={profile.name}
|
||||||
onChange={e => setProfile({...profile, name: e.target.value })} />
|
onChange={e => setProfile({...profile, name: e.target.value })} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</ProfileFormGroup>
|
||||||
<FormGroup>
|
<ProfileFormGroup>
|
||||||
<FormLabel>{L("account.full_name")}</FormLabel>
|
<FormLabel>{L("account.full_name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TextField variant={"outlined"}
|
<TextField variant={"outlined"}
|
||||||
@ -96,46 +203,129 @@ export default function ProfileView(props) {
|
|||||||
value={profile.fullName ?? ""}
|
value={profile.fullName ?? ""}
|
||||||
onChange={e => setProfile({...profile, fullName: e.target.value })} />
|
onChange={e => setProfile({...profile, fullName: e.target.value })} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</ProfileFormGroup>
|
||||||
<h4>{L("account.change_password")}</h4>
|
|
||||||
<FormGroup>
|
<CollapseBox title={L("account.change_password")} open={openedTab === "password"}
|
||||||
<FormLabel>{L("account.old_password")}</FormLabel>
|
onToggle={() => setOpenedTab(openedTab === "password" ? "" : "password")}
|
||||||
<FormControl>
|
icon={<Password />}>
|
||||||
<TextField variant={"outlined"}
|
<ProfileFormGroup>
|
||||||
size={"small"}
|
<FormLabel>{L("account.password_old")}</FormLabel>
|
||||||
type={"password"}
|
<FormControl>
|
||||||
placeholder={L("general.unchanged")}
|
<TextField variant={"outlined"}
|
||||||
value={changePassword.old}
|
size={"small"}
|
||||||
onChange={e => setChangePassword({...changePassword, old: e.target.value })} />
|
type={"password"}
|
||||||
</FormControl>
|
placeholder={L("general.unchanged")}
|
||||||
</FormGroup>
|
value={changePassword.old}
|
||||||
<FormGroup>
|
onChange={e => setChangePassword({...changePassword, old: e.target.value })} />
|
||||||
<FormLabel>{L("account.new_password")}</FormLabel>
|
</FormControl>
|
||||||
<FormControl>
|
</ProfileFormGroup>
|
||||||
<TextField variant={"outlined"}
|
<ProfileFormGroup>
|
||||||
size={"small"}
|
<FormLabel>{L("account.password_new")}</FormLabel>
|
||||||
type={"password"}
|
<FormControl>
|
||||||
value={changePassword.new}
|
<TextField variant={"outlined"}
|
||||||
onChange={e => setChangePassword({...changePassword, new: e.target.value })} />
|
size={"small"}
|
||||||
</FormControl>
|
type={"password"}
|
||||||
</FormGroup>
|
value={changePassword.new}
|
||||||
<FormGroup>
|
onChange={e => setChangePassword({...changePassword, new: e.target.value })} />
|
||||||
<FormLabel>{L("account.confirm_password")}</FormLabel>
|
</FormControl>
|
||||||
<FormControl>
|
</ProfileFormGroup>
|
||||||
<TextField variant={"outlined"}
|
<ProfileFormGroup>
|
||||||
size={"small"}
|
<FormLabel>{L("account.password_confirm")}</FormLabel>
|
||||||
type={"password"}
|
<FormControl>
|
||||||
placeholder={L("general.unchanged")}
|
<TextField variant={"outlined"}
|
||||||
value={changePassword.confirm}
|
size={"small"}
|
||||||
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
type={"password"}
|
||||||
</FormControl>
|
value={changePassword.confirm}
|
||||||
</FormGroup>
|
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
||||||
<Button variant={"outlined"} color={"primary"}
|
</FormControl>
|
||||||
disabled={isSaving || !api.hasPermission("user/updateProfile")}
|
</ProfileFormGroup>
|
||||||
startIcon={isSaving ? <CircularProgress size={12} /> : <Save />}
|
</CollapseBox>
|
||||||
onClick={onUpdateProfile}>
|
|
||||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
<CollapseBox title={L("account.gpg_key")} open={openedTab === "gpg"}
|
||||||
</Button>
|
onToggle={() => setOpenedTab(openedTab === "gpg" ? "" : "gpg")}
|
||||||
|
icon={<VpnKey />}>
|
||||||
|
{
|
||||||
|
profile.gpgKey ? <Box>
|
||||||
|
<GpgFingerprintBox mb={2}>
|
||||||
|
{ profile.gpgKey.confirmed ?
|
||||||
|
<CheckCircle color={"info"} title={L("account.gpg_key_confirmed")} /> :
|
||||||
|
<ErrorOutline color={"secondary"} title={L("account.gpg_key_pending")} />
|
||||||
|
}
|
||||||
|
GPG-Fingerprint: <code title={L("general.click_to_copy")} onClick={() => navigator.clipboard.writeText(profile.gpgKey.fingerprint)}>
|
||||||
|
{profile.gpgKey.fingerprint}
|
||||||
|
</code>
|
||||||
|
</GpgFingerprintBox>
|
||||||
|
<ProfileFormGroup>
|
||||||
|
<FormLabel>{L("account.password")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TextField variant={"outlined"} size={"small"}
|
||||||
|
value={gpgKeyPassword} type={"password"}
|
||||||
|
onChange={e => setGpgKeyPassword(e.target.value)}
|
||||||
|
placeholder={L("account.password")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</ProfileFormGroup>
|
||||||
|
<Button startIcon={isGpgKeyRemoving ? <CircularProgress size={12} /> : <Remove />}
|
||||||
|
color={"secondary"} onClick={onRemoveGpgKey}
|
||||||
|
variant={"outlined"} size={"small"}
|
||||||
|
disabled={isGpgKeyRemoving || !api.hasPermission("user/removeGPG")}>
|
||||||
|
{isGpgKeyRemoving ? L("general.removing") + "…" : L("general.remove")}
|
||||||
|
</Button>
|
||||||
|
</Box> :
|
||||||
|
<Box>
|
||||||
|
<ProfileFormGroup>
|
||||||
|
<FormLabel>{L("account.gpg_key")}</FormLabel>
|
||||||
|
<GpgKeyField value={gpgKey} multiline={true} rows={8}
|
||||||
|
disabled={isGpgKeyUploading || !api.hasPermission("user/importGPG")}
|
||||||
|
placeholder={L("account.gpg_key_placeholder_text")}
|
||||||
|
onChange={e => setGpgKey(e.target.value)}
|
||||||
|
onDrop={e => {
|
||||||
|
let file = e.dataTransfer.files[0];
|
||||||
|
getFileContents(file, (data) => {
|
||||||
|
setGpgKey(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}}/>
|
||||||
|
</ProfileFormGroup>
|
||||||
|
<ButtonBar>
|
||||||
|
<Button size={"small"}
|
||||||
|
variant={"outlined"}
|
||||||
|
startIcon={<CloudUpload />}
|
||||||
|
component={"label"}>
|
||||||
|
Upload file
|
||||||
|
<VisuallyHiddenInput type={"file"} onChange={e => {
|
||||||
|
let file = e.target.files[0];
|
||||||
|
getFileContents(file, (data) => {
|
||||||
|
setGpgKey(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}} />
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={isGpgKeyUploading ? <CircularProgress size={12} /> : <Upload />}
|
||||||
|
color={"primary"} onClick={onUploadGPG}
|
||||||
|
variant={"outlined"} size={"small"}
|
||||||
|
disabled={isGpgKeyUploading || !api.hasPermission("user/importGPG")}>
|
||||||
|
{isGpgKeyUploading ? L("general.uploading") + "…" : L("general.upload")}
|
||||||
|
</Button>
|
||||||
|
</ButtonBar>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</CollapseBox>
|
||||||
|
|
||||||
|
<CollapseBox title={L("account.2fa_token")} open={openedTab === "2fa"}
|
||||||
|
onToggle={() => setOpenedTab(openedTab === "2fa" ? "" : "2fa")}
|
||||||
|
icon={<Fingerprint />}>
|
||||||
|
<b>test</b>
|
||||||
|
</CollapseBox>
|
||||||
|
|
||||||
|
<Box mt={2}>
|
||||||
|
<Button variant={"outlined"} color={"primary"}
|
||||||
|
disabled={isSaving || !api.hasPermission("user/updateProfile")}
|
||||||
|
startIcon={isSaving ? <CircularProgress size={12} /> : <Save />}
|
||||||
|
onClick={onUpdateProfile}>
|
||||||
|
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
@ -10,12 +10,7 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import RouteForm from "./route-form";
|
import RouteForm from "./route-form";
|
||||||
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||||
|
import ButtonBar from "../../elements/button-bar";
|
||||||
const ButtonBar = styled(Box)((props) => ({
|
|
||||||
"& > button": {
|
|
||||||
marginRight: props.theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const MonoSpaceTextField = styled(TextField)((props) => ({
|
const MonoSpaceTextField = styled(TextField)((props) => ({
|
||||||
"& input": {
|
"& input": {
|
||||||
|
@ -27,17 +27,12 @@ import {
|
|||||||
SettingsApplications
|
SettingsApplications
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import TIME_ZONES from "shared/time-zones";
|
import TIME_ZONES from "shared/time-zones";
|
||||||
|
import ButtonBar from "../elements/button-bar";
|
||||||
|
|
||||||
const SettingsFormGroup = styled(FormGroup)((props) => ({
|
const SettingsFormGroup = styled(FormGroup)((props) => ({
|
||||||
marginBottom: props.theme.spacing(1),
|
marginBottom: props.theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ButtonBar = styled(Box)((props) => ({
|
|
||||||
"& > button": {
|
|
||||||
marginRight: props.theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SettingsView(props) {
|
export default function SettingsView(props) {
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
@ -180,7 +175,12 @@ export default function SettingsView(props) {
|
|||||||
api.sendTestMail(testMailAddress).then(data => {
|
api.sendTestMail(testMailAddress).then(data => {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
showDialog(data.msg, L("settings.send_test_email_error"));
|
showDialog(<>
|
||||||
|
{data.msg} <br />
|
||||||
|
<code>
|
||||||
|
{data.output}
|
||||||
|
</code>
|
||||||
|
</>, L("settings.send_test_email_error"));
|
||||||
} else {
|
} else {
|
||||||
showDialog(L("settings.send_test_email_success"), L("general.success"));
|
showDialog(L("settings.send_test_email_success"), L("general.success"));
|
||||||
setTestMailAddress("");
|
setTestMailAddress("");
|
||||||
@ -220,7 +220,7 @@ export default function SettingsView(props) {
|
|||||||
<TextField size={"small"} variant={"outlined"}
|
<TextField size={"small"} variant={"outlined"}
|
||||||
type={"password"}
|
type={"password"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={"(" + L("settings.unchanged") + ")"}
|
placeholder={"(" + L("general.unchanged") + ")"}
|
||||||
value={settings[key_name]}
|
value={settings[key_name]}
|
||||||
onChange={e => onChangeValue(key_name, e.target.value)} />
|
onChange={e => onChangeValue(key_name, e.target.value)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -227,7 +227,7 @@ export class SecretsColumn extends DataColumn {
|
|||||||
properties.className = clsx(properties.className, "font-monospace");
|
properties.className = clsx(properties.className, "font-monospace");
|
||||||
|
|
||||||
if (this.canCopy) {
|
if (this.canCopy) {
|
||||||
properties.title = L("Click to copy");
|
properties.title = L("general.click_to_copy");
|
||||||
properties.className = clsx(properties.className, "data-table-clickable");
|
properties.className = clsx(properties.className, "data-table-clickable");
|
||||||
properties.onClick = () => {
|
properties.onClick = () => {
|
||||||
navigator.clipboard.writeText(originalData);
|
navigator.clipboard.writeText(originalData);
|
||||||
|
Loading…
Reference in New Issue
Block a user