Mail bugfix, gpg, profile frontend WIP
This commit is contained in:
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);
|
||||
|
||||
Reference in New Issue
Block a user