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\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\GpgKey;
|
||||
use PhpParser\Node\Param;
|
||||
|
||||
class Test extends MailAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
parent::__construct($context, $externalCall, [
|
||||
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL),
|
||||
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null)
|
||||
));
|
||||
]);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
@ -74,10 +75,12 @@ namespace Core\API\Mail {
|
||||
"subject" => "Test E-Mail",
|
||||
"body" => "Hey! If you receive this e-mail, your mail configuration seems to be working.",
|
||||
"gpgFingerprint" => $this->getParam("gpgFingerprint"),
|
||||
"async" => false
|
||||
"async" => false,
|
||||
"debug" => true,
|
||||
));
|
||||
|
||||
$this->lastError = $req->getLastError();
|
||||
$this->result["output"] = $req->getResult()["output"];
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
@ -95,7 +98,8 @@ namespace Core\API\Mail {
|
||||
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
|
||||
'replyName' => new StringType('replyName', 32, true, ""),
|
||||
'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;
|
||||
}
|
||||
@ -115,6 +119,7 @@ namespace Core\API\Mail {
|
||||
$replyName = $this->getParam('replyName');
|
||||
$body = $this->getParam('body');
|
||||
$gpgFingerprint = $this->getParam("gpgFingerprint");
|
||||
$debug = $this->getParam("debug");
|
||||
|
||||
$mailAsync = $this->getParam("async");
|
||||
if ($mailAsync === null) {
|
||||
@ -156,8 +161,9 @@ namespace Core\API\Mail {
|
||||
$mail->addReplyTo($replyTo, $replyName);
|
||||
}
|
||||
|
||||
|
||||
$mail->Subject = $subject;
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->SMTPDebug = $debug ? 2 : 0;
|
||||
$mail->Host = $mailConfig->getHost();
|
||||
$mail->Port = $mailConfig->getPort();
|
||||
$mail->SMTPAuth = true;
|
||||
@ -193,12 +199,22 @@ namespace Core\API\Mail {
|
||||
$mail->AltBody = strip_tags($body);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$this->success = @$mail->Send();
|
||||
$output = ob_get_contents();
|
||||
ob_end_clean();
|
||||
if (!$this->success) {
|
||||
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
|
||||
$this->logger->error("sendMail() failed: $mail->ErrorInfo");
|
||||
if ($debug) {
|
||||
$this->logger->debug($output);
|
||||
$this->result["output"] = $output;
|
||||
}
|
||||
} else {
|
||||
$this->result["messageId"] = $mail->getLastMessageID();
|
||||
if ($debug) {
|
||||
$this->result["output"] = $output;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->success = false;
|
||||
|
@ -1240,9 +1240,9 @@ namespace Core\API\User {
|
||||
class ImportGPG extends UserAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
parent::__construct($context, $externalCall, [
|
||||
"pubkey" => new StringType("pubkey")
|
||||
));
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->forbidMethod("GET");
|
||||
}
|
||||
@ -1342,6 +1342,10 @@ namespace Core\API\User {
|
||||
|
||||
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 {
|
||||
@ -1371,6 +1375,10 @@ namespace Core\API\User {
|
||||
|
||||
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 {
|
||||
|
@ -85,4 +85,15 @@ return [
|
||||
"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_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",
|
||||
"no" => "Nein",
|
||||
"create_new" => "Erstellen",
|
||||
"unchanged" => "Unverändert",
|
||||
|
||||
# dialog / actions
|
||||
"action" => "Aktion",
|
||||
|
@ -12,17 +12,6 @@ return [
|
||||
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
||||
"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
|
||||
"key" => "Schlüssel",
|
||||
"value" => "Wert",
|
||||
@ -30,7 +19,6 @@ return [
|
||||
"mail" => "Mail",
|
||||
"recaptcha" => "reCaptcha",
|
||||
"uncategorized" => "Unkategorisiert",
|
||||
"unchanged" => "Unverändert",
|
||||
|
||||
# general settings
|
||||
"site_name" => "Seitenname",
|
||||
|
@ -85,4 +85,15 @@ return [
|
||||
"remove_group_member_text" => "Do you really want to remove user '%s' from this group?",
|
||||
"add_group_member_title" => "Add member",
|
||||
"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",
|
||||
"no" => "No",
|
||||
"create_new" => "Create",
|
||||
"unchanged" => "Unchanged",
|
||||
|
||||
# dialog / actions
|
||||
"action" => "Action",
|
||||
|
@ -12,17 +12,6 @@ return [
|
||||
"show_only_active_keys" => "Show only active keys",
|
||||
"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
|
||||
"key" => "Key",
|
||||
"value" => "Value",
|
||||
@ -30,7 +19,6 @@ return [
|
||||
"mail" => "Mail",
|
||||
"recaptcha" => "reCaptcha",
|
||||
"uncategorized" => "Uncategorized",
|
||||
"unchanged" => "Unchanged",
|
||||
|
||||
# general settings
|
||||
"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)
|
||||
- [Account & User functions](#access-control)
|
||||
- Admin Dashboard
|
||||
- File Sharing Dashboard
|
||||
- Docker Support
|
||||
|
||||
### Upcoming:
|
||||
|
30
cli.php
30
cli.php
@ -630,8 +630,33 @@ function onMail($argv): void {
|
||||
if (!$req->execute(["debug" => $debug])) {
|
||||
_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 {
|
||||
_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();
|
||||
|
||||
$userId = $argv[2];
|
||||
if (!is_numeric($userId)) {
|
||||
$res = $sql->select("id")
|
||||
@ -932,7 +956,7 @@ $registeredCommands = [
|
||||
"routes" => ["handler" => "onRoutes", "description" => "view and modify routes"],
|
||||
"maintenance" => ["handler" => "onMaintenance", "description" => "toggle maintenance mode"],
|
||||
"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"],
|
||||
"impersonate" => ["handler" => "onImpersonate", "description" => "create a session and print cookies and csrf tokens", "requiresDocker" => true],
|
||||
"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 usePagination from "shared/hooks/pagination";
|
||||
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 {MuiColorInput} from "mui-color-input";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
|
||||
const defaultGroupData = {
|
||||
name: "",
|
||||
@ -17,12 +18,6 @@ const defaultGroupData = {
|
||||
members: []
|
||||
};
|
||||
|
||||
const ButtonBar = styled(Box)((props) => ({
|
||||
"& > button": {
|
||||
marginRight: props.theme.spacing(1)
|
||||
}
|
||||
}));
|
||||
|
||||
export default function EditGroupView(props) {
|
||||
|
||||
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 React, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {Button, CircularProgress, FormControl, FormGroup, FormLabel, TextField} from "@mui/material";
|
||||
import {Save} from "@mui/icons-material";
|
||||
import {
|
||||
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) {
|
||||
|
||||
@ -22,9 +76,14 @@ export default function ProfileView(props) {
|
||||
// data
|
||||
const [profile, setProfile] = useState({...api.user});
|
||||
const [changePassword, setChangePassword] = useState({ old: "", new: "", confirm: "" });
|
||||
const [gpgKey, setGpgKey] = useState("");
|
||||
const [gpgKeyPassword, setGpgKeyPassword] = useState("");
|
||||
|
||||
// ui
|
||||
const [openedTab, setOpenedTab] = useState(null);
|
||||
const [isSaving, setSaving] = useState(false);
|
||||
const [isGpgKeyUploading, setGpgKeyUploading] = useState(false);
|
||||
const [isGpgKeyRemoving, setGpgKeyRemoving] = useState(false);
|
||||
|
||||
const onUpdateProfile = useCallback(() => {
|
||||
|
||||
@ -60,6 +119,54 @@ export default function ProfileView(props) {
|
||||
|
||||
}, [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 <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
@ -79,7 +186,7 @@ export default function ProfileView(props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<FormGroup>
|
||||
<ProfileFormGroup>
|
||||
<FormLabel>{L("account.username")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
@ -87,8 +194,8 @@ export default function ProfileView(props) {
|
||||
value={profile.name}
|
||||
onChange={e => setProfile({...profile, name: e.target.value })} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
</ProfileFormGroup>
|
||||
<ProfileFormGroup>
|
||||
<FormLabel>{L("account.full_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
@ -96,46 +203,129 @@ export default function ProfileView(props) {
|
||||
value={profile.fullName ?? ""}
|
||||
onChange={e => setProfile({...profile, fullName: e.target.value })} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<h4>{L("account.change_password")}</h4>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.old_password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
placeholder={L("general.unchanged")}
|
||||
value={changePassword.old}
|
||||
onChange={e => setChangePassword({...changePassword, old: e.target.value })} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.new_password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
value={changePassword.new}
|
||||
onChange={e => setChangePassword({...changePassword, new: e.target.value })} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.confirm_password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
placeholder={L("general.unchanged")}
|
||||
value={changePassword.confirm}
|
||||
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<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>
|
||||
</ProfileFormGroup>
|
||||
|
||||
<CollapseBox title={L("account.change_password")} open={openedTab === "password"}
|
||||
onToggle={() => setOpenedTab(openedTab === "password" ? "" : "password")}
|
||||
icon={<Password />}>
|
||||
<ProfileFormGroup>
|
||||
<FormLabel>{L("account.password_old")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
placeholder={L("general.unchanged")}
|
||||
value={changePassword.old}
|
||||
onChange={e => setChangePassword({...changePassword, old: e.target.value })} />
|
||||
</FormControl>
|
||||
</ProfileFormGroup>
|
||||
<ProfileFormGroup>
|
||||
<FormLabel>{L("account.password_new")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
value={changePassword.new}
|
||||
onChange={e => setChangePassword({...changePassword, new: e.target.value })} />
|
||||
</FormControl>
|
||||
</ProfileFormGroup>
|
||||
<ProfileFormGroup>
|
||||
<FormLabel>{L("account.password_confirm")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
type={"password"}
|
||||
value={changePassword.confirm}
|
||||
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
||||
</FormControl>
|
||||
</ProfileFormGroup>
|
||||
</CollapseBox>
|
||||
|
||||
<CollapseBox title={L("account.gpg_key")} open={openedTab === "gpg"}
|
||||
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>
|
||||
</>
|
||||
}
|
@ -10,12 +10,7 @@ import {
|
||||
import * as React from "react";
|
||||
import RouteForm from "./route-form";
|
||||
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||
|
||||
const ButtonBar = styled(Box)((props) => ({
|
||||
"& > button": {
|
||||
marginRight: props.theme.spacing(1)
|
||||
}
|
||||
}));
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
|
||||
const MonoSpaceTextField = styled(TextField)((props) => ({
|
||||
"& input": {
|
||||
|
@ -27,17 +27,12 @@ import {
|
||||
SettingsApplications
|
||||
} from "@mui/icons-material";
|
||||
import TIME_ZONES from "shared/time-zones";
|
||||
import ButtonBar from "../elements/button-bar";
|
||||
|
||||
const SettingsFormGroup = styled(FormGroup)((props) => ({
|
||||
marginBottom: props.theme.spacing(1),
|
||||
}));
|
||||
|
||||
const ButtonBar = styled(Box)((props) => ({
|
||||
"& > button": {
|
||||
marginRight: props.theme.spacing(1)
|
||||
}
|
||||
}));
|
||||
|
||||
export default function SettingsView(props) {
|
||||
|
||||
// meta
|
||||
@ -180,7 +175,12 @@ export default function SettingsView(props) {
|
||||
api.sendTestMail(testMailAddress).then(data => {
|
||||
setSending(false);
|
||||
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 {
|
||||
showDialog(L("settings.send_test_email_success"), L("general.success"));
|
||||
setTestMailAddress("");
|
||||
@ -220,7 +220,7 @@ export default function SettingsView(props) {
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
type={"password"}
|
||||
disabled={disabled}
|
||||
placeholder={"(" + L("settings.unchanged") + ")"}
|
||||
placeholder={"(" + L("general.unchanged") + ")"}
|
||||
value={settings[key_name]}
|
||||
onChange={e => onChangeValue(key_name, e.target.value)} />
|
||||
</FormControl>
|
||||
|
@ -227,7 +227,7 @@ export class SecretsColumn extends DataColumn {
|
||||
properties.className = clsx(properties.className, "font-monospace");
|
||||
|
||||
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.onClick = () => {
|
||||
navigator.clipboard.writeText(originalData);
|
||||
|
Loading…
Reference in New Issue
Block a user