|
@@ -6,81 +6,18 @@ import {
|
|
|
Button,
|
|
|
CircularProgress,
|
|
|
FormControl,
|
|
|
- FormGroup,
|
|
|
- FormLabel, styled,
|
|
|
+ FormLabel,
|
|
|
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";
|
|
|
-import MfaTotp from "./mfa-totp";
|
|
|
-import MfaFido from "./mfa-fido";
|
|
|
import Dialog from "shared/elements/dialog";
|
|
|
-import PasswordStrength from "shared/elements/password-strength";
|
|
|
import SpacedFormGroup from "../../elements/form-group";
|
|
|
-
|
|
|
-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 MFAOptions = styled(Box)((props) => ({
|
|
|
- "& > div": {
|
|
|
- borderColor: props.theme.palette.divider,
|
|
|
- borderStyle: "solid",
|
|
|
- borderWidth: 1,
|
|
|
- borderRadius: 5,
|
|
|
- maxWidth: 150,
|
|
|
- cursor: "pointer",
|
|
|
- textAlign: "center",
|
|
|
- display: "inline-grid",
|
|
|
- gridTemplateRows: "130px 50px",
|
|
|
- alignItems: "center",
|
|
|
- padding: props.theme.spacing(1),
|
|
|
- marginRight: props.theme.spacing(1),
|
|
|
- "&:hover": {
|
|
|
- backgroundColor: "lightgray",
|
|
|
- },
|
|
|
- "& img": {
|
|
|
- width: "100%",
|
|
|
- },
|
|
|
- }
|
|
|
-}));
|
|
|
-
|
|
|
-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,
|
|
|
-});
|
|
|
+import ChangePasswordBox from "./change-password-box";
|
|
|
+import GpgBox from "./gpg-box";
|
|
|
+import MultiFactorBox from "./mfa-box";
|
|
|
+import EditProfilePicture from "./edit-picture";
|
|
|
|
|
|
export default function ProfileView(props) {
|
|
|
|
|
@@ -100,16 +37,10 @@ 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("");
|
|
|
- const [mfaPassword, set2FAPassword] = useState("");
|
|
|
|
|
|
// ui
|
|
|
const [openedTab, setOpenedTab] = useState(null);
|
|
|
const [isSaving, setSaving] = useState(false);
|
|
|
- const [isGpgKeyUploading, setGpgKeyUploading] = useState(false);
|
|
|
- const [isGpgKeyRemoving, setGpgKeyRemoving] = useState(false);
|
|
|
- const [is2FARemoving, set2FARemoving] = useState(false);
|
|
|
const [dialogData, setDialogData] = useState({show: false});
|
|
|
|
|
|
const onUpdateProfile = useCallback(() => {
|
|
@@ -146,69 +77,6 @@ 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, profile]);
|
|
|
-
|
|
|
- const onRemove2FA = useCallback(() => {
|
|
|
- if (!is2FARemoving) {
|
|
|
- set2FARemoving(true);
|
|
|
- api.remove2FA(mfaPassword).then(data => {
|
|
|
- set2FARemoving(false);
|
|
|
- set2FAPassword("");
|
|
|
- if (!data.success) {
|
|
|
- showDialog(data.msg, L("account.remove_2fa_error"));
|
|
|
- } else {
|
|
|
- setProfile({...profile, twoFactorToken: null});
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }, [api, showDialog, is2FARemoving, mfaPassword, profile]);
|
|
|
-
|
|
|
- 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"}>
|
|
@@ -227,173 +95,56 @@ export default function ProfileView(props) {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className={"content"}>
|
|
|
- <SpacedFormGroup>
|
|
|
- <FormLabel>{L("account.username")}</FormLabel>
|
|
|
- <FormControl>
|
|
|
- <TextField variant={"outlined"}
|
|
|
- size={"small"}
|
|
|
- value={profile.name}
|
|
|
- onChange={e => setProfile({...profile, name: e.target.value })} />
|
|
|
- </FormControl>
|
|
|
- </SpacedFormGroup>
|
|
|
- <SpacedFormGroup>
|
|
|
- <FormLabel>{L("account.full_name")}</FormLabel>
|
|
|
- <FormControl>
|
|
|
- <TextField variant={"outlined"}
|
|
|
- size={"small"}
|
|
|
- value={profile.fullName ?? ""}
|
|
|
- onChange={e => setProfile({...profile, fullName: e.target.value })} />
|
|
|
- </FormControl>
|
|
|
- </SpacedFormGroup>
|
|
|
-
|
|
|
- <CollapseBox title={L("account.change_password")} open={openedTab === "password"}
|
|
|
- onToggle={() => setOpenedTab(openedTab === "password" ? "" : "password")}
|
|
|
- icon={<Password />}>
|
|
|
- <SpacedFormGroup>
|
|
|
- <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>
|
|
|
- </SpacedFormGroup>
|
|
|
- <SpacedFormGroup>
|
|
|
- <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>
|
|
|
- </SpacedFormGroup>
|
|
|
- <SpacedFormGroup>
|
|
|
- <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>
|
|
|
- </SpacedFormGroup>
|
|
|
- <Box className={"w-50"}>
|
|
|
- <PasswordStrength password={changePassword.new} minLength={6} />
|
|
|
+ <Box>
|
|
|
+ <Box display={"grid"} gridTemplateColumns={"300px auto"}>
|
|
|
+ <EditProfilePicture api={api} showDialog={showDialog} setProfile={setProfile}
|
|
|
+ profile={profile} setDialogData={setDialogData} />
|
|
|
+ <Box p={2}>
|
|
|
+ <SpacedFormGroup>
|
|
|
+ <FormLabel>{L("account.username")}</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <TextField variant={"outlined"}
|
|
|
+ size={"small"}
|
|
|
+ value={profile.name}
|
|
|
+ onChange={e => setProfile({...profile, name: e.target.value })} />
|
|
|
+ </FormControl>
|
|
|
+ </SpacedFormGroup>
|
|
|
+ <SpacedFormGroup>
|
|
|
+ <FormLabel>{L("account.full_name")}</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <TextField variant={"outlined"}
|
|
|
+ size={"small"}
|
|
|
+ value={profile.fullName ?? ""}
|
|
|
+ onChange={e => setProfile({...profile, fullName: e.target.value })} />
|
|
|
+ </FormControl>
|
|
|
+ </SpacedFormGroup>
|
|
|
+ <SpacedFormGroup>
|
|
|
+ <FormLabel>{L("account.email")}</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <TextField variant={"outlined"}
|
|
|
+ size={"small"}
|
|
|
+ value={profile.email ?? ""}
|
|
|
+ disabled={true}/>
|
|
|
+ </FormControl>
|
|
|
+ </SpacedFormGroup>
|
|
|
</Box>
|
|
|
- </CollapseBox>
|
|
|
+ </Box>
|
|
|
|
|
|
- <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>
|
|
|
- <SpacedFormGroup>
|
|
|
- <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>
|
|
|
- </SpacedFormGroup>
|
|
|
- <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>
|
|
|
- <SpacedFormGroup>
|
|
|
- <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;
|
|
|
- }}/>
|
|
|
- </SpacedFormGroup>
|
|
|
- <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>
|
|
|
+ <ChangePasswordBox open={openedTab === "password"}
|
|
|
+ onToggle={() => setOpenedTab(openedTab === "password" ? "" : "password")}
|
|
|
+ changePassword={changePassword}
|
|
|
+ setChangePassword={setChangePassword} />
|
|
|
|
|
|
- <CollapseBox title={L("account.2fa_token")} open={openedTab === "2fa"}
|
|
|
- onToggle={() => setOpenedTab(openedTab === "2fa" ? "" : "2fa")}
|
|
|
- icon={<Fingerprint />}>
|
|
|
- {profile.twoFactorToken && profile.twoFactorToken.confirmed ?
|
|
|
- <Box>
|
|
|
- <GpgFingerprintBox mb={2}>
|
|
|
- { profile.twoFactorToken.confirmed ?
|
|
|
- <CheckCircle color={"info"} title={L("account.gpg_key_confirmed")} /> :
|
|
|
- <ErrorOutline color={"secondary"} title={L("account.gpg_key_pending")} />
|
|
|
- }
|
|
|
- {L("account.2fa_type_" + profile.twoFactorToken.type)}
|
|
|
- </GpgFingerprintBox>
|
|
|
- <SpacedFormGroup>
|
|
|
- <FormLabel>{L("account.password")}</FormLabel>
|
|
|
- <FormControl>
|
|
|
- <TextField variant={"outlined"} size={"small"}
|
|
|
- value={mfaPassword} type={"password"}
|
|
|
- onChange={e => set2FAPassword(e.target.value)}
|
|
|
- placeholder={L("account.password")}
|
|
|
- />
|
|
|
- </FormControl>
|
|
|
- </SpacedFormGroup>
|
|
|
- <Button startIcon={is2FARemoving ? <CircularProgress size={12} /> : <Remove />}
|
|
|
- color={"secondary"} onClick={onRemove2FA}
|
|
|
- variant={"outlined"} size={"small"}
|
|
|
- disabled={is2FARemoving || !api.hasPermission("tfa/remove")}>
|
|
|
- {is2FARemoving ? L("general.removing") + "…" : L("general.remove")}
|
|
|
- </Button>
|
|
|
- </Box> :
|
|
|
- <MFAOptions>
|
|
|
- <MfaTotp api={api} showDialog={showDialog} setDialogData={setDialogData}
|
|
|
- set2FA={token => setProfile({...profile, twoFactorToken: token })} />
|
|
|
- <MfaFido api={api} showDialog={showDialog} setDialogData={setDialogData}
|
|
|
- set2FA={token => setProfile({...profile, twoFactorToken: token })} />
|
|
|
- </MFAOptions>
|
|
|
- }
|
|
|
- </CollapseBox>
|
|
|
+ <GpgBox open={openedTab === "gpg"}
|
|
|
+ onToggle={() => setOpenedTab(openedTab === "gpg" ? "" : "gpg")}
|
|
|
+ profile={profile} setProfile={setProfile}
|
|
|
+ api={api} showDialog={showDialog} />
|
|
|
+
|
|
|
+ <MultiFactorBox open={openedTab === "2fa"}
|
|
|
+ onToggle={() => setOpenedTab(openedTab === "2fa" ? "" : "2fa")}
|
|
|
+ profile={profile} setProfile={setProfile}
|
|
|
+ setDialogData={setDialogData}
|
|
|
+ api={api} showDialog={showDialog} />
|
|
|
|
|
|
<Box mt={2}>
|
|
|
<Button variant={"outlined"} color={"primary"}
|
|
@@ -403,7 +154,7 @@ export default function ProfileView(props) {
|
|
|
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
|
|
</Button>
|
|
|
</Box>
|
|
|
- </div>
|
|
|
+ </Box>
|
|
|
|
|
|
<Dialog show={dialogData.show}
|
|
|
title={dialogData.title}
|