From ea9f220eefcd69454721e92c808bab221258c3fd Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Apr 2023 21:54:31 +0200 Subject: [PATCH] feat: Add setup pgp --- public/locales/de-DE/settings.json | 2 +- public/locales/en-US/settings-email-pgp.json | 20 +++ public/locales/en-US/settings.json | 3 +- src/App.tsx | 5 + .../SettingsEmailPGPRoute/ImportKeyDialog.tsx | 71 +++++++++ .../SetupPGPEncryptionForm.tsx | 149 ++++++++++++++++++ src/routes/SettingsEmailPGPRoute.tsx | 29 ++++ src/routes/SettingsRoute.tsx | 7 + src/server-types.ts | 1 + 9 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 public/locales/en-US/settings-email-pgp.json create mode 100644 src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx create mode 100644 src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx create mode 100644 src/routes/SettingsEmailPGPRoute.tsx diff --git a/public/locales/de-DE/settings.json b/public/locales/de-DE/settings.json index bc710ff..227caf4 100644 --- a/public/locales/de-DE/settings.json +++ b/public/locales/de-DE/settings.json @@ -2,6 +2,6 @@ "title": "Einstellungen", "actions": { "enable2fa": "Zwei-Faktor-Authentifizierung", - "aliasPreferences": "Alias-Präferenzen" + "aliasPreferences": "Alias-Präferenzen", } } diff --git a/public/locales/en-US/settings-email-pgp.json b/public/locales/en-US/settings-email-pgp.json new file mode 100644 index 0000000..4f86dc5 --- /dev/null +++ b/public/locales/en-US/settings-email-pgp.json @@ -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:" +} diff --git a/public/locales/en-US/settings.json b/public/locales/en-US/settings.json index 756d4aa..2b4d23b 100644 --- a/public/locales/en-US/settings.json +++ b/public/locales/en-US/settings.json @@ -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" } } diff --git a/src/App.tsx b/src/App.tsx index 48a0768..8a44dad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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: , }, + { + path: "/settings/email-pgp", + element: , + }, { path: "/reports", loader: getServerSettings, diff --git a/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx b/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx new file mode 100644 index 0000000..386f14b --- /dev/null +++ b/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx @@ -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 ( + + {t("findPublicKey.title")} + + + {t("findPublicKey.description", { + createdAt: publicKeyResult?.createdAt, + type: publicKeyResult?.type, + })} + + + {isLoadingFingerprint ? : {fingerprint}} + + + + + + + + ) +} diff --git a/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx new file mode 100644 index 0000000..f44a887 --- /dev/null +++ b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx @@ -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(null) + + const schema = yup.object().shape({ + publicKey: yup.string().label(t("form.publicKey.label")), + }) + const {user, _updateUser} = useContext(AuthContext) + + const {mutateAsync} = useMutation( + updatePreferences, + { + onSuccess: (response, values) => { + const newUser = { + ...user, + preferences: { + ...user!.preferences, + ...values, + }, + } as User + + if (response.detail) { + showSuccess(response?.detail) + } + + _updateUser(newUser) + }, + }, + ) + + const formik = useFormik
({ + 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 ( + <> + + + + + + + {t("form.fields.publicKey.helperText")} + + + + } + onClick={() => findPublicKeyAsync()} + > + {t("findPublicKey.label")} + + + + + } + > + {t("form.continueActionLabel")} + + + +
+ setPublicKeyResult(null)} + onImport={() => { + formik.setFieldValue("publicKey", publicKeyResult!.publicKey || "") + setPublicKeyResult(null) + }} + /> + + ) +} diff --git a/src/routes/SettingsEmailPGPRoute.tsx b/src/routes/SettingsEmailPGPRoute.tsx new file mode 100644 index 0000000..3b9a1ae --- /dev/null +++ b/src/routes/SettingsEmailPGPRoute.tsx @@ -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 ( + + + + ) + } else { + return ( + + + {t("alreadyConfigured")} + + + ) + } +} diff --git a/src/routes/SettingsRoute.tsx b/src/routes/SettingsRoute.tsx index a103fe6..9b1f293 100644 --- a/src/routes/SettingsRoute.tsx +++ b/src/routes/SettingsRoute.tsx @@ -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 { + + + + + + ) diff --git a/src/server-types.ts b/src/server-types.ts index ca6ca57..b0efbcf 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -54,6 +54,7 @@ export interface ServerUser { aliasProxyUserAgent: ProxyUserAgentType aliasExpandUrlShorteners: boolean aliasRejectOnPrivacyLeak: boolean + emailGpgPublicKey: string | null } }