feat: Add setup pgp

This commit is contained in:
Myzel394 2023-04-09 21:54:31 +02:00
parent 2a1fbf4bd8
commit ea9f220eef
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
9 changed files with 285 additions and 2 deletions

View File

@ -2,6 +2,6 @@
"title": "Einstellungen",
"actions": {
"enable2fa": "Zwei-Faktor-Authentifizierung",
"aliasPreferences": "Alias-Präferenzen"
"aliasPreferences": "Alias-Präferenzen",
}
}

View File

@ -0,0 +1,20 @@
{
"title": "Set up PGP encryption",
"description": "By providing your public key, we will encrypt all emails sent to you. This will ensure that only you can read the emails we send you. Your email provider will not be able to read your emails anymore.",
"form": {
"fields": {
"publicKey": {
"label": "Your public key",
"helperText": "Paste your raw public key in armored format here."
}
},
"continueActionLabel": "Enable PGP encryption"
},
"findPublicKey": {
"label": "Find public key automatically",
"title": "Use this key?",
"description": "We found a public key for your email! Would you like to use it? The key has been created on {{createdAt}} and is of type {{type}}. This is the fingerprint:",
"continueActionLabel": "Use Key"
},
"alreadyConfigured": "PGP encryption is activated. You are using a public key with this fingerprint:"
}

View File

@ -3,6 +3,7 @@
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences",
"apiKeys": "Manage API Keys"
"apiKeys": "Manage API Keys",
"emailPgp": "PGP Encryption for Emails"
}
}

View File

@ -32,6 +32,7 @@ import RootRoute from "~/routes/Root"
import Settings2FARoute from "~/routes/Settings2FARoute"
import SettingsAPIKeysRoute from "~/routes/SettingsAPIKeysRoute"
import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute"
import SettingsEmailPGPRoute from "~/routes/SettingsEmailPGPRoute"
import SettingsRoute from "~/routes/SettingsRoute"
import SignupRoute from "~/routes/SignupRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
@ -113,6 +114,10 @@ const router = createBrowserRouter([
loader: getServerSettings,
element: <SettingsAPIKeysRoute />,
},
{
path: "/settings/email-pgp",
element: <SettingsEmailPGPRoute />,
},
{
path: "/reports",
loader: getServerSettings,

View File

@ -0,0 +1,71 @@
import {ReactElement} from "react"
import {useTranslation} from "react-i18next"
import {HiKey} from "react-icons/hi"
import {TiCancel} from "react-icons/ti"
import {useAsync} from "react-use"
import {readKey} from "openpgp"
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material"
import {FindPublicKeyResponse} from "~/apis"
export interface ImportKeyDialogProps {
open: boolean
publicKeyResult: FindPublicKeyResponse | null
onClose: () => void
onImport: () => void
}
export default function ImportKeyDialog({
open,
publicKeyResult,
onClose,
onImport,
}: ImportKeyDialogProps): ReactElement {
const {t} = useTranslation(["settings-email-pgp", "common"])
const {value: fingerprint, loading: isLoadingFingerprint} = useAsync(async () => {
if (publicKeyResult === null) {
return
}
const key = await readKey({
armoredKey: publicKeyResult!.publicKey,
})
return key.getFingerprint()
}, [publicKeyResult])
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("findPublicKey.title")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("findPublicKey.description", {
createdAt: publicKeyResult?.createdAt,
type: publicKeyResult?.type,
})}
</DialogContentText>
<Box my={2}>
{isLoadingFingerprint ? <CircularProgress /> : <code>{fingerprint}</code>}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} startIcon={<TiCancel />}>
{t("general.cancelLabel", {ns: "common"})}
</Button>
<Button onClick={onImport} startIcon={<HiKey />} variant="contained">
{t("findPublicKey.continueActionLabel")}
</Button>
</DialogActions>
</Dialog>
)
}

View File

@ -0,0 +1,149 @@
import * as yup from "yup"
import {ReactElement, useContext, useState} from "react"
import {useFormik} from "formik"
import {useMutation} from "@tanstack/react-query"
import {Box, FormGroup, FormHelperText, Grid, TextField} from "@mui/material"
import {
FindPublicKeyResponse,
UpdatePreferencesData,
findPublicKey,
updatePreferences,
} from "~/apis"
import {HiSearch} from "react-icons/hi"
import {LoadingButton} from "@mui/lab"
import {parseFastAPIError} from "~/utils"
import {AxiosError} from "axios"
import {RiLockFill} from "react-icons/ri"
import {SimpleDetailResponse, User} from "~/server-types"
import {useTranslation} from "react-i18next"
import {useErrorSuccessSnacks} from "~/hooks"
import {AuthContext} from "~/components"
import ImportKeyDialog from "./ImportKeyDialog"
interface Form {
publicKey: string
detail?: string
}
export default function SetupPGPEncryptionForm(): ReactElement {
const {t} = useTranslation(["settings-email-pgp", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks()
const [publicKeyResult, setPublicKeyResult] = useState<FindPublicKeyResponse | null>(null)
const schema = yup.object().shape({
publicKey: yup.string().label(t("form.publicKey.label")),
})
const {user, _updateUser} = useContext(AuthContext)
const {mutateAsync} = useMutation<SimpleDetailResponse, AxiosError, UpdatePreferencesData>(
updatePreferences,
{
onSuccess: (response, values) => {
const newUser = {
...user,
preferences: {
...user!.preferences,
...values,
},
} as User
if (response.detail) {
showSuccess(response?.detail)
}
_updateUser(newUser)
},
},
)
const formik = useFormik<Form>({
validationSchema: schema,
initialValues: {
publicKey: user?.preferences.emailGpgPublicKey || "",
},
onSubmit: async (values, {setErrors}) => {
try {
await mutateAsync({
emailGpgPublicKey: values.publicKey,
})
} catch (error) {
setErrors(parseFastAPIError(error as AxiosError))
}
},
})
const {mutateAsync: findPublicKeyAsync, isLoading: isFindingPublicKey} = useMutation<
FindPublicKeyResponse,
AxiosError,
void
>(findPublicKey, {
onSuccess: setPublicKeyResult,
onError: showError,
})
return (
<>
<form onSubmit={formik.handleSubmit}>
<Grid container spacing={4} direction="column" alignItems="stretch">
<Grid item xs={12}>
<FormGroup
title={t("form.fields.publicKey.title")}
key="publicKey"
sx={{width: "100%"}}
>
<TextField
fullWidth
multiline
minRows={5}
maxRows={20}
label={t("form.fields.publicKey.label")}
name="publicKey"
value={formik.values.publicKey}
onChange={formik.handleChange}
error={Boolean(formik.errors.publicKey)}
helperText={formik.errors.publicKey}
disabled={formik.isSubmitting}
/>
<FormHelperText
error={Boolean(formik.touched.publicKey && formik.errors.publicKey)}
>
{t("form.fields.publicKey.helperText")}
</FormHelperText>
</FormGroup>
<Box mt={1}>
<LoadingButton
loading={isFindingPublicKey}
type="submit"
startIcon={<HiSearch />}
onClick={() => findPublicKeyAsync()}
>
{t("findPublicKey.label")}
</LoadingButton>
</Box>
</Grid>
<Grid item alignSelf="center">
<LoadingButton
loading={formik.isSubmitting}
type="submit"
variant="contained"
disabled={!formik.isValid}
startIcon={<RiLockFill />}
>
{t("form.continueActionLabel")}
</LoadingButton>
</Grid>
</Grid>
</form>
<ImportKeyDialog
open={Boolean(publicKeyResult)}
publicKeyResult={publicKeyResult}
onClose={() => setPublicKeyResult(null)}
onImport={() => {
formik.setFieldValue("publicKey", publicKeyResult!.publicKey || "")
setPublicKeyResult(null)
}}
/>
</>
)
}

View File

@ -0,0 +1,29 @@
import {ReactElement} from "react"
import {useTranslation} from "react-i18next"
import {Alert} from "@mui/material"
import {SimplePage} from "~/components"
import {useUser} from "~/hooks"
import SetupPGPEncryptionForm from "~/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm"
export default function SettingsEmailPGPRoute(): ReactElement {
const {t} = useTranslation(["settings-email-pgp", "common"])
const user = useUser()
if (!user?.preferences.emailGpgPublicKey) {
return (
<SimplePage title={t("title")} description={t("description")}>
<SetupPGPEncryptionForm />
</SimplePage>
)
} else {
return (
<SimplePage title={t("title")} description={t("description")}>
<Alert severity="success" variant="standard">
{t("alreadyConfigured")}
</Alert>
</SimplePage>
)
}
}

View File

@ -8,6 +8,7 @@ import React, {ReactElement} from "react"
import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
import {SimplePageBuilder} from "~/components"
import {RiLockFill} from "react-icons/ri"
export default function SettingsRoute(): ReactElement {
const {t} = useTranslation("settings")
@ -33,6 +34,12 @@ export default function SettingsRoute(): ReactElement {
</ListItemIcon>
<ListItemText primary={t("actions.apiKeys")} />
</ListItemButton>
<ListItemButton component={Link} to="/settings/email-pgp">
<ListItemIcon>
<RiLockFill />
</ListItemIcon>
<ListItemText primary={t("actions.emailPgp")} />
</ListItemButton>
</List>
</SimplePageBuilder.Page>
)

View File

@ -54,6 +54,7 @@ export interface ServerUser {
aliasProxyUserAgent: ProxyUserAgentType
aliasExpandUrlShorteners: boolean
aliasRejectOnPrivacyLeak: boolean
emailGpgPublicKey: string | null
}
}