This commit is contained in:
Myzel394 2022-11-02 12:36:54 +01:00
parent 2840645b94
commit a7eece7938
12 changed files with 214 additions and 190 deletions

View File

@ -226,6 +226,10 @@
"saveAction": "Save preferences" "saveAction": "Save preferences"
} }
} }
},
"LogoutRoute": {
"title": "Log out",
"description": "We are logging you out..."
} }
}, },
@ -240,6 +244,9 @@
"signup": "Sign up", "signup": "Sign up",
"login": "Log in" "login": "Log in"
}, },
"AuthenticatedRoute": {
"logout": "Logout"
},
"EnterDecryptionPassword": { "EnterDecryptionPassword": {
"title": "Decrypt Reports", "title": "Decrypt Reports",
"description": "Please enter your password so that your reports can de decrypted.", "description": "Please enter your password so that your reports can de decrypted.",

View File

@ -16,6 +16,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
import CompleteAccountRoute from "~/routes/CompleteAccountRoute" import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword" import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
import LoginRoute from "~/routes/LoginRoute" import LoginRoute from "~/routes/LoginRoute"
import LogoutRoute from "~/routes/LogoutRoute"
import ReportDetailRoute from "~/routes/ReportDetailRoute" import ReportDetailRoute from "~/routes/ReportDetailRoute"
import ReportsRoute from "~/routes/ReportsRoute" import ReportsRoute from "~/routes/ReportsRoute"
import RootRoute from "~/routes/Root" import RootRoute from "~/routes/Root"
@ -54,6 +55,10 @@ const router = createBrowserRouter([
path: "/auth/complete-account", path: "/auth/complete-account",
element: <CompleteAccountRoute />, element: <CompleteAccountRoute />,
}, },
{
path: "/auth/logout",
element: <LogoutRoute />,
},
], ],
}, },
{ {

View File

@ -2,16 +2,12 @@ import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react"
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {decrypt, readMessage, readPrivateKey} from "openpgp" import {decrypt, readMessage, readPrivateKey} from "openpgp"
import {useNavigate} from "react-router-dom"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {ServerUser, User} from "~/server-types" import {ServerUser, User} from "~/server-types"
import { import {REFRESH_TOKEN_URL, RefreshTokenResult, logout as logoutUser, refreshToken} from "~/apis"
REFRESH_TOKEN_URL,
RefreshTokenResult,
logout as logoutUser,
refreshToken,
} from "~/apis"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {decryptString, encryptString} from "~/utils" import {decryptString, encryptString} from "~/utils"
@ -21,20 +17,15 @@ export interface AuthContextProviderProps {
children: ReactNode children: ReactNode
} }
export default function AuthContextProvider({ export default function AuthContextProvider({children}: AuthContextProviderProps): ReactElement {
children, const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, {
}: AuthContextProviderProps): ReactElement {
const {mutateAsync: refresh} = useMutation<
RefreshTokenResult,
AxiosError,
void
>(refreshToken, {
onError: () => logout(false), onError: () => logout(false),
}) })
const [decryptionPassword, setDecryptionPassword] = useLocalStorage< const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>(
string | null "_global-context-auth-decryption-password",
>("_global-context-auth-decryption-password", null) null,
)
const [user, setUser] = useLocalStorage<ServerUser | User | null>( const [user, setUser] = useLocalStorage<ServerUser | User | null>(
"_global-context-auth-user", "_global-context-auth-user",
null, null,
@ -115,10 +106,7 @@ export default function AuthContextProvider({
try { try {
// Check if the password is correct // Check if the password is correct
const masterPassword = decryptString( const masterPassword = decryptString(user.encryptedPassword, password)
user.encryptedPassword,
password,
)
JSON.parse(decryptString(user.encryptedNotes, masterPassword)) JSON.parse(decryptString(user.encryptedNotes, masterPassword))
} catch { } catch {
return false return false
@ -162,15 +150,8 @@ export default function AuthContextProvider({
// Decrypt user notes // Decrypt user notes
useEffect(() => { useEffect(() => {
if ( if (user && !user.isDecrypted && user.encryptedPassword && masterPassword) {
user && const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!))
!user.isDecrypted &&
user.encryptedPassword &&
masterPassword
) {
const note = JSON.parse(
decryptUsingMasterPassword(user.encryptedNotes!),
)
const newUser: User = { const newUser: User = {
...user, ...user,

View File

@ -2,6 +2,8 @@ import {ReactElement} from "react"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useKeyPress} from "react-use" import {useKeyPress} from "react-use"
import {MdContentCopy} from "react-icons/md" import {MdContentCopy} from "react-icons/md"
import {useSnackbar} from "notistack"
import {Link as RouterLink} from "react-router-dom"
import copy from "copy-to-clipboard" import copy from "copy-to-clipboard"
import { import {
@ -16,7 +18,6 @@ import {
import {AliasTypeIndicator} from "~/components" import {AliasTypeIndicator} from "~/components"
import {AliasList} from "~/server-types" import {AliasList} from "~/server-types"
import {useUIState} from "~/hooks" import {useUIState} from "~/hooks"
import {useSnackbar} from "notistack"
import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values" import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values"
import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton" import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton"
import EmptyStateScreen from "~/route-widgets/AliasesRoute/EmptyStateScreen" import EmptyStateScreen from "~/route-widgets/AliasesRoute/EmptyStateScreen"
@ -41,6 +42,7 @@ export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactEle
<List> <List>
{aliasesUIState.map(alias => ( {aliasesUIState.map(alias => (
<ListItemButton <ListItemButton
component={RouterLink}
key={alias.id} key={alias.id}
onClick={event => { onClick={event => {
if (isInCopyAddressMode) { if (isInCopyAddressMode) {
@ -60,7 +62,7 @@ export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactEle
) )
} }
}} }}
href={`/aliases/${btoa(getAddress(alias))}`} to={`/aliases/${btoa(getAddress(alias))}`}
> >
<ListItemIcon> <ListItemIcon>
<AliasTypeIndicator type={alias.type} /> <AliasTypeIndicator type={alias.type} />

View File

@ -11,7 +11,7 @@ import {LoadingButton} from "@mui/lab"
import {Box, Grid, InputAdornment, Typography} from "@mui/material" import {Box, Grid, InputAdornment, Typography} from "@mui/material"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {PasswordField} from "~/components" import {PasswordField, SimpleForm} from "~/components"
import {buildEncryptionPassword, encryptString} from "~/utils" import {buildEncryptionPassword, encryptString} from "~/utils"
import {isDev} from "~/constants/development" import {isDev} from "~/constants/development"
import {useSystemPreferredTheme, useUser} from "~/hooks" import {useSystemPreferredTheme, useUser} from "~/hooks"
@ -68,7 +68,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
{ {
onSuccess: ({user}) => { onSuccess: ({user}) => {
login(user) login(user)
onDone() setTimeout(onDone, 0)
}, },
}, },
) )
@ -119,32 +119,17 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
return ( return (
<Box width="80vw"> <Box width="80vw">
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<Grid <SimpleForm
container title={t("routes.CompleteAccountRoute.forms.password.title")}
spacing={4} description={t("routes.CompleteAccountRoute.forms.password.description")}
paddingX={2} continueActionLabel={t(
paddingY={4} "routes.CompleteAccountRoute.forms.password.continueAction",
alignItems="center" )}
justifyContent="center" nonFieldError={formik.errors.detail}
> >
<Grid item> {[
<Grid container spacing={2} direction="column">
<Grid item>
<Typography variant="h6" component="h2" align="center">
{t("routes.CompleteAccountRoute.forms.password.title")}
</Typography>
</Grid>
<Grid item>
<Typography variant="subtitle1" component="p">
{t("routes.CompleteAccountRoute.forms.password.description")}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container spacing={2} justifyContent="center">
<Grid item>
<PasswordField <PasswordField
key="password"
fullWidth fullWidth
id="password" id="password"
name="password" name="password"
@ -158,9 +143,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
value={formik.values.password} value={formik.values.password}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
error={ error={formik.touched.password && Boolean(formik.errors.password)}
formik.touched.password && Boolean(formik.errors.password)
}
helperText={formik.touched.password && formik.errors.password} helperText={formik.touched.password && formik.errors.password}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
@ -169,10 +152,9 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />,
</Grid>
<Grid item>
<PasswordField <PasswordField
key="passwordConfirmation"
fullWidth fullWidth
id="passwordConfirmation" id="passwordConfirmation"
name="passwordConfirmation" name="passwordConfirmation"
@ -200,21 +182,9 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />,
</Grid> ]}
</Grid> </SimpleForm>
</Grid>
<Grid item>
<LoadingButton
type="submit"
variant="contained"
loading={formik.isSubmitting}
startIcon={<MdChevronRight />}
>
{t("routes.CompleteAccountRoute.forms.password.continueAction")}
</LoadingButton>
</Grid>
</Grid>
</form> </form>
</Box> </Box>
) )

View File

@ -54,7 +54,8 @@ export default function ConfirmCodeForm({
return code.split("").every(char => chars.includes(char)) return code.split("").every(char => chars.includes(char))
}, },
), )
.label(t("routes.LoginRoute.forms.confirmCode.form.code.label")),
}) })
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyLoginWithEmailData>( const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyLoginWithEmailData>(
verifyLoginWithEmail, verifyLoginWithEmail,

View File

@ -16,7 +16,7 @@ export default function EmptyStateScreen(): ReactElement {
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Icon path={mdiTextBoxMultiple} size={2.5} />, <Icon path={mdiTextBoxMultiple} size={2.5} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">

View File

@ -1,7 +1,10 @@
import {ReactElement} from "react" import {ReactElement} from "react"
import {Outlet} from "react-router-dom" import {Outlet} from "react-router-dom"
import {Link as RouterLink} from "react-router-dom"
import {useTranslation} from "react-i18next"
import {MdLogout} from "react-icons/md"
import {Box, Grid, List, ListItem, Paper, useTheme} from "@mui/material" import {Box, Button, Grid, List, ListItem, Paper, useTheme} from "@mui/material"
import {useUser} from "~/hooks" import {useUser} from "~/hooks"
import LockNavigationContextProvider from "~/LockNavigationContext/LockNavigationContextProvider" import LockNavigationContextProvider from "~/LockNavigationContext/LockNavigationContextProvider"
@ -9,11 +12,12 @@ import NavigationButton, {
NavigationSection, NavigationSection,
} from "~/route-widgets/AuthenticateRoute/NavigationButton" } from "~/route-widgets/AuthenticateRoute/NavigationButton"
const sections = ( const sections = (Object.keys(NavigationSection) as Array<keyof typeof NavigationSection>).filter(
Object.keys(NavigationSection) as Array<keyof typeof NavigationSection> value => isNaN(Number(value)),
).filter(value => isNaN(Number(value))) )
export default function AuthenticatedRoute(): ReactElement { export default function AuthenticatedRoute(): ReactElement {
const {t} = useTranslation()
const theme = useTheme() const theme = useTheme()
useUser() useUser()
@ -31,42 +35,65 @@ export default function AuthenticatedRoute(): ReactElement {
display="flex" display="flex"
maxWidth="90vw" maxWidth="90vw"
width="100%" width="100%"
height="100%"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
> >
<Grid <Grid
maxWidth="md" maxWidth="md"
container container
height="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
> >
<Grid item xs={12} sm={3} md={2}> <Grid item xs={12} sm={3} md={2}>
<Box <Box bgcolor={theme.palette.background.paper}>
bgcolor={theme.palette.background.paper} <List component="nav">
component="nav"
>
<List>
{sections.map(key => ( {sections.map(key => (
<ListItem key={key}> <ListItem key={key}>
<NavigationButton <NavigationButton section={NavigationSection[key]} />
section={NavigationSection[key]}
/>
</ListItem> </ListItem>
))} ))}
</List> </List>
</Box> </Box>
</Grid> </Grid>
<Grid item xs={12} sm={9} md={10}> <Grid item xs={12} sm={9} md={10} height="100%">
<Paper> <Grid
<Box container
maxHeight="80vh" direction="column"
sx={{overflowY: "auto"}} height="100%"
padding={4} justifyContent="space-between"
> >
<Grid item></Grid>
<Grid item>
<Paper>
<Box maxHeight="80vh" sx={{overflowY: "auto"}} padding={4}>
<Outlet /> <Outlet />
</Box> </Box>
</Paper> </Paper>
</Grid> </Grid>
<Grid item>
<Grid
container
spacing={2}
justifyContent="center"
marginBottom={2}
>
<Grid item>
<Button
component={RouterLink}
color="inherit"
size="small"
to="/auth/logout"
startIcon={<MdLogout />}
>
{t("components.AuthenticatedRoute.logout")}
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid> </Grid>
</Box> </Box>
</Box> </Box>

View File

@ -29,7 +29,10 @@ export default function CompleteAccountRoute(): ReactElement {
onYes={() => setShowGenerationReportForm(true)} onYes={() => setShowGenerationReportForm(true)}
onNo={navigateToNext} onNo={navigateToNext}
/>, />,
<PasswordForm onDone={navigateToNext} key="password_form" />, <PasswordForm
onDone={() => setTimeout(navigateToNext, 0)}
key="password_form"
/>,
]} ]}
index={showGenerationReportForm ? 1 : 0} index={showGenerationReportForm ? 1 : 0}
/> />

View File

@ -31,10 +31,7 @@ export default function EnterDecryptionPassword(): ReactElement {
password: "", password: "",
}, },
onSubmit: async ({password}, {setErrors}) => { onSubmit: async ({password}, {setErrors}) => {
const decryptionPassword = buildEncryptionPassword( const decryptionPassword = buildEncryptionPassword(password, user.email.address)
password,
user.email.address,
)
if (!_setDecryptionPassword(decryptionPassword)) { if (!_setDecryptionPassword(decryptionPassword)) {
setErrors({ setErrors({
@ -43,7 +40,7 @@ export default function EnterDecryptionPassword(): ReactElement {
), ),
}) })
} else { } else {
navigateToNext() setTimeout(navigateToNext, 0)
} }
}, },
}) })
@ -52,15 +49,9 @@ export default function EnterDecryptionPassword(): ReactElement {
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<SimpleForm <SimpleForm
title={t("components.EnterDecryptionPassword.title")} title={t("components.EnterDecryptionPassword.title")}
description={t( description={t("components.EnterDecryptionPassword.description")}
"components.EnterDecryptionPassword.description", cancelActionLabel={t("components.EnterDecryptionPassword.cancelAction")}
)} continueActionLabel={t("components.EnterDecryptionPassword.continueAction")}
cancelActionLabel={t(
"components.EnterDecryptionPassword.cancelAction",
)}
continueActionLabel={t(
"components.EnterDecryptionPassword.continueAction",
)}
isSubmitting={formik.isSubmitting} isSubmitting={formik.isSubmitting}
> >
{[ {[
@ -69,22 +60,15 @@ export default function EnterDecryptionPassword(): ReactElement {
fullWidth fullWidth
name="password" name="password"
id="password" id="password"
label={t( label={t("components.EnterDecryptionPassword.form.password.label")}
"components.EnterDecryptionPassword.form.password.label",
)}
placeholder={t( placeholder={t(
"components.EnterDecryptionPassword.form.password.placeholder", "components.EnterDecryptionPassword.form.password.placeholder",
)} )}
value={formik.values.password} value={formik.values.password}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
error={ error={formik.touched.password && Boolean(formik.errors.password)}
formik.touched.password && helperText={formik.touched.password && formik.errors.password}
Boolean(formik.errors.password)
}
helperText={
formik.touched.password && formik.errors.password
}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">

View File

@ -1,5 +1,6 @@
import {ReactElement, useContext, useState} from "react" import {ReactElement, useContext, useState} from "react"
import {useNavigate} from "react-router-dom" import {useNavigate} from "react-router-dom"
import {useUpdateEffect} from "react-use"
import {MultiStepForm} from "~/components" import {MultiStepForm} from "~/components"
import AuthContext from "~/AuthContext/AuthContext" import AuthContext from "~/AuthContext/AuthContext"
@ -8,11 +9,23 @@ import EmailForm from "~/route-widgets/LoginRoute/EmailForm"
export default function LoginRoute(): ReactElement { export default function LoginRoute(): ReactElement {
const navigate = useNavigate() const navigate = useNavigate()
const {login} = useContext(AuthContext) const {login, user} = useContext(AuthContext)
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
const [sameRequestToken, setSameRequestToken] = useState<string>("") const [sameRequestToken, setSameRequestToken] = useState<string>("")
useUpdateEffect(() => {
if (!user) {
return
}
if (user?.encryptedPassword) {
navigate("/enter-password")
} else {
navigate("/")
}
}, [user?.encryptedPassword])
return ( return (
<MultiStepForm <MultiStepForm
steps={[ steps={[
@ -25,17 +38,7 @@ export default function LoginRoute(): ReactElement {
/>, />,
<ConfirmCodeForm <ConfirmCodeForm
key="confirm_code_form" key="confirm_code_form"
onConfirm={user => { onConfirm={login}
login(user)
setTimeout(() => {
if (user.encryptedPassword) {
navigate("/enter-password")
} else {
navigate("/")
}
}, 0)
}}
email={email} email={email}
sameRequestToken={sameRequestToken} sameRequestToken={sameRequestToken}
/>, />,

View File

@ -0,0 +1,41 @@
import {ReactElement, useContext} from "react"
import {useTranslation} from "react-i18next"
import {useEffectOnce} from "react-use"
import {Box, CircularProgress, Grid, Paper, Typography} from "@mui/material"
import {useNavigateToNext} from "~/hooks"
import AuthContext from "~/AuthContext/AuthContext"
export default function LogoutRoute(): ReactElement {
const {t} = useTranslation()
const navigateToNext = useNavigateToNext("/auth/login")
const {logout} = useContext(AuthContext)
useEffectOnce(() => {
logout()
navigateToNext()
})
return (
<Paper>
<Box padding={4}>
<Grid container spacing={4} direction="column" alignItems="center">
<Grid item>
<Typography variant="h6" component="h1">
{t("routes.LogoutRoute.title")}
</Typography>
</Grid>
<Grid item>
<CircularProgress />
</Grid>
<Grid item>
<Typography variant="body1">
{t("routes.LogoutRoute.description")}
</Typography>
</Grid>
</Grid>
</Box>
</Paper>
)
}