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..1795266
--- /dev/null
+++ b/public/locales/en-US/settings-email-pgp.json
@@ -0,0 +1,22 @@
+{
+ "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.",
+ "placeholder": "----BEGIN PGP PUBLIC KEY BLOCK----\n\n...\n----END PGP PUBLIC KEY BLOCK----"
+ }
+ },
+ "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: ",
+ "remove": "Disable PGP encryption"
+}
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..43cf6f7 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,11 @@ const router = createBrowserRouter([
loader: getServerSettings,
element: ,
},
+ {
+ path: "/settings/email-pgp",
+ loader: getServerSettings,
+ element: ,
+ },
{
path: "/reports",
loader: getServerSettings,
diff --git a/src/apis/find-public-key.ts b/src/apis/find-public-key.ts
new file mode 100644
index 0000000..46bf918
--- /dev/null
+++ b/src/apis/find-public-key.ts
@@ -0,0 +1,19 @@
+import {client} from "~/constants/axios-client"
+
+export interface FindPublicKeyResponse {
+ publicKey: string
+ type: string
+ createdAt: Date
+}
+
+export default async function findPublicKey(): Promise {
+ const {data} = await client.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/v1/preferences/find-public-key`,
+ {},
+ {
+ withCredentials: true,
+ },
+ )
+
+ return data
+}
diff --git a/src/apis/index.ts b/src/apis/index.ts
index ce3aceb..7d6bbb7 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -74,3 +74,5 @@ export * from "./get-api-keys"
export {default as getAPIKeys} from "./get-api-keys"
export * from "./delete-api-key"
export {default as deleteAPIKey} from "./delete-api-key"
+export * from "./find-public-key"
+export {default as findPublicKey} from "./find-public-key"
diff --git a/src/apis/update-preferences.ts b/src/apis/update-preferences.ts
index c3b90f8..f30080a 100644
--- a/src/apis/update-preferences.ts
+++ b/src/apis/update-preferences.ts
@@ -9,6 +9,7 @@ export interface UpdatePreferencesData {
aliasProxyUserAgent?: ProxyUserAgentType
aliasExpandUrlShorteners?: boolean
aliasRejectOnPrivacyLeak?: boolean
+ emailGpgPublicKey?: string | null
}
export default async function updatePreferences(
diff --git a/src/route-widgets/AliasDetailRoute/SelectField.tsx b/src/route-widgets/AliasDetailRoute/SelectField.tsx
index 3490576..3f5d0ae 100644
--- a/src/route-widgets/AliasDetailRoute/SelectField.tsx
+++ b/src/route-widgets/AliasDetailRoute/SelectField.tsx
@@ -40,10 +40,7 @@ export default function SelectField({
const labelId = `${name}-label`
const preferenceName = `alias${name.charAt(0).toUpperCase() + name.slice(1)}`
const value = user.preferences[preferenceName as keyof User["preferences"]]
- console.log(user.preferences)
- console.log(preferenceName)
- console.log(value)
- const defaultValueText = valueTextMap[value.toString()]
+ const defaultValueText = valueTextMap[value!.toString()]
return (
diff --git a/src/route-widgets/SettingsEmailPGPRoute/AlreadyConfigured.tsx b/src/route-widgets/SettingsEmailPGPRoute/AlreadyConfigured.tsx
new file mode 100644
index 0000000..00a9511
--- /dev/null
+++ b/src/route-widgets/SettingsEmailPGPRoute/AlreadyConfigured.tsx
@@ -0,0 +1,76 @@
+import {AxiosError} from "axios"
+import {useAsync} from "react-use"
+import {readKey} from "openpgp"
+import {FaLockOpen} from "react-icons/fa"
+import {ReactElement, useContext} from "react"
+import {useTranslation} from "react-i18next"
+
+import {Alert, CircularProgress, Grid} from "@mui/material"
+import {LoadingButton} from "@mui/lab"
+import {useMutation} from "@tanstack/react-query"
+
+import {SimpleDetailResponse, User} from "~/server-types"
+import {UpdatePreferencesData, updatePreferences} from "~/apis"
+import {useErrorSuccessSnacks} from "~/hooks"
+import {AuthContext} from "~/components"
+
+export default function AlreadyConfigured(): ReactElement {
+ const {t} = useTranslation(["settings-email-pgp", "common"])
+ const {user, _updateUser} = useContext(AuthContext)
+ const {showSuccess, showError} = useErrorSuccessSnacks()
+
+ const {mutateAsync, isLoading} = 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)
+ },
+ onError: showError,
+ })
+ const {value: fingerprint, loading: isLoadingFingerprint} = useAsync(async () => {
+ const key = await readKey({
+ armoredKey: user!.preferences.emailGpgPublicKey!,
+ })
+
+ return key.getFingerprint()
+ }, [user?.preferences?.emailGpgPublicKey])
+
+ return (
+
+
+
+ {t("alreadyConfigured")}
+ {isLoadingFingerprint ? : {fingerprint}
}
+
+
+
+ }
+ onClick={() =>
+ mutateAsync({
+ emailGpgPublicKey: null,
+ })
+ }
+ >
+ {t("remove")}
+
+
+
+ )
+}
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 (
+
+ )
+}
diff --git a/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx
new file mode 100644
index 0000000..5f6877e
--- /dev/null
+++ b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx
@@ -0,0 +1,157 @@
+import * as yup from "yup"
+import {useLoaderData} from "react-router-dom"
+import {ReactElement, useContext, useState} from "react"
+import {useFormik} from "formik"
+import {HiSearch} from "react-icons/hi"
+import {AxiosError} from "axios"
+import {RiLockFill} from "react-icons/ri"
+import {useTranslation} from "react-i18next"
+
+import {LoadingButton} from "@mui/lab"
+import {useMutation} from "@tanstack/react-query"
+import {Box, FormGroup, FormHelperText, Grid, TextField} from "@mui/material"
+
+import {
+ FindPublicKeyResponse,
+ UpdatePreferencesData,
+ findPublicKey,
+ updatePreferences,
+} from "~/apis"
+import {parseFastAPIError} from "~/utils"
+import {ServerSettings, SimpleDetailResponse, User} from "~/server-types"
+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 serverSettings = useLoaderData() as ServerSettings
+ const {showSuccess, showError} = useErrorSuccessSnacks()
+
+ const [publicKeyResult, setPublicKeyResult] = useState(null)
+
+ const schema = yup.object().shape({
+ publicKey: yup.string().label(t("form.publicKey.label")).min(1),
+ })
+ 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
+ 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..e6e9889
--- /dev/null
+++ b/src/routes/SettingsEmailPGPRoute.tsx
@@ -0,0 +1,22 @@
+import {ReactElement} from "react"
+import {useTranslation} from "react-i18next"
+
+import {SimplePage} from "~/components"
+import {useUser} from "~/hooks"
+import AlreadyConfigured from "~/route-widgets/SettingsEmailPGPRoute/AlreadyConfigured"
+import SetupPGPEncryptionForm from "~/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm"
+
+export default function SettingsEmailPGPRoute(): ReactElement {
+ const {t} = useTranslation(["settings-email-pgp", "common"])
+ const user = useUser()
+
+ return (
+
+ {user?.preferences.emailGpgPublicKey ? (
+
+ ) : (
+
+ )}
+
+ )
+}
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..3fb2513 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
}
}
@@ -87,6 +88,7 @@ export interface ServerSettings {
allowAliasDeletion: boolean
apiKeyMaxDays: number
allowRegistrations: boolean
+ allowPgpKeyDiscovery: boolean
}
export interface Alias {