mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-18 23:45:26 +02:00
feat: Add setup pgp
This commit is contained in:
parent
2a1fbf4bd8
commit
ea9f220eef
@ -2,6 +2,6 @@
|
||||
"title": "Einstellungen",
|
||||
"actions": {
|
||||
"enable2fa": "Zwei-Faktor-Authentifizierung",
|
||||
"aliasPreferences": "Alias-Präferenzen"
|
||||
"aliasPreferences": "Alias-Präferenzen",
|
||||
}
|
||||
}
|
||||
|
20
public/locales/en-US/settings-email-pgp.json
Normal file
20
public/locales/en-US/settings-email-pgp.json
Normal 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:"
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
71
src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx
Normal file
71
src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
29
src/routes/SettingsEmailPGPRoute.tsx
Normal file
29
src/routes/SettingsEmailPGPRoute.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
|
@ -54,6 +54,7 @@ export interface ServerUser {
|
||||
aliasProxyUserAgent: ProxyUserAgentType
|
||||
aliasExpandUrlShorteners: boolean
|
||||
aliasRejectOnPrivacyLeak: boolean
|
||||
emailGpgPublicKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user