improvements

This commit is contained in:
Myzel394 2022-10-30 18:31:15 +01:00
parent 64a8ae3096
commit c6634dc740
14 changed files with 296 additions and 197 deletions

View File

@ -23,6 +23,8 @@
"date-fns": "^2.29.3",
"formik": "^2.2.9",
"group-array": "^1.0.0",
"immutability-helper": "^3.1.1",
"in-milliseconds": "^1.2.0",
"in-seconds": "^1.2.0",
"openpgp": "^5.5.0",
"react": "^18.2.0",

View File

@ -2,11 +2,18 @@ import {createContext} from "react"
import {ServerUser, User} from "~/server-types"
export enum EncryptionStatus {
Unavailable = "Unavailable",
PasswordRequired = "PasswordRequired",
Available = "Available",
}
interface AuthContextTypeBase {
user: ServerUser | User | null
isAuthenticated: boolean
login: (user: ServerUser | User) => void
logout: () => void
encryptionStatus: EncryptionStatus
_decryptUsingMasterPassword: (content: string) => string
_encryptUsingMasterPassword: (content: string) => string
_decryptUsingPrivateKey: (message: string) => Promise<string>
@ -31,6 +38,7 @@ export type AuthContextType =
const AuthContext = createContext<AuthContextType>({
user: null,
isAuthenticated: false,
encryptionStatus: EncryptionStatus.Unavailable,
login: () => {
throw new Error("login() not implemented")
},

View File

@ -15,7 +15,7 @@ import {
import {client} from "~/constants/axios-client"
import {decryptString, encryptString} from "~/utils"
import AuthContext, {AuthContextType} from "./AuthContext"
import AuthContext, {AuthContextType, EncryptionStatus} from "./AuthContext"
export interface AuthContextProviderProps {
children: ReactNode
@ -134,6 +134,21 @@ export default function AuthContextProvider({
() => ({
user: user ?? null,
login: setUser,
encryptionStatus: (() => {
if (!user) {
return EncryptionStatus.Unavailable
}
if (!user.encryptedPassword) {
return EncryptionStatus.Unavailable
}
if (user.isDecrypted) {
return EncryptionStatus.Available
}
return EncryptionStatus.PasswordRequired
})(),
logout,
isAuthenticated: user !== null,
_encryptUsingMasterPassword: encryptUsingMasterPassword,

View File

@ -1,31 +1,85 @@
import {ReactElement, useContext} from "react"
import {useContext} from "react"
import {MdLock} from "react-icons/md"
import {Link as RouterLink} from "react-router-dom"
import {Alert, Button, Grid} from "@mui/material"
import {Button, Grid, Typography} from "@mui/material"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
import LockNavigationContext from "~/LockNavigationContext/LockNavigationContext"
export default function DecryptionPasswordMissingAlert(): ReactElement {
const {handleAnchorClick} = useContext(LockNavigationContext)
export interface WithEncryptionRequiredProps {
children?: JSX.Element
}
export default function DecryptionPasswordMissingAlert({
children = <></>,
}: WithEncryptionRequiredProps): JSX.Element {
const {handleAnchorClick} = useContext(LockNavigationContext)
const {encryptionStatus} = useContext(AuthContext)
switch (encryptionStatus) {
case EncryptionStatus.Unavailable: {
return (
<Grid container spacing={2} direction="column" alignItems="center">
<Grid container spacing={4}>
<Grid item>
<Alert severity="warning">
Your decryption password is required to view this section.
</Alert>
<Typography variant="h6" component="h2">
Encryption required
</Typography>
</Grid>
<Grid item>
<Typography>
You need to set up encryption to use this feature.
</Typography>
</Grid>
<Grid item>
<Button
startIcon={<MdLock />}
variant="contained"
component={RouterLink}
to="/enter-password"
to="/complete-account?setup=true"
startIcon={<MdLock />}
onClick={handleAnchorClick}
>
Enter password
Setup encryption
</Button>
</Grid>
</Grid>
)
}
case EncryptionStatus.PasswordRequired: {
return (
<Grid
container
spacing={4}
direction="column"
alignItems="center"
>
<Grid item>
<Typography variant="h6" component="h2">
Password required
</Typography>
</Grid>
<Grid item>
<Typography>
Your decryption password is required to view this
section.
</Typography>
</Grid>
<Grid item>
<Button
component={RouterLink}
to={`/enter-password?next=${window.location.pathname}`}
startIcon={<MdLock />}
onClick={handleAnchorClick}
>
Enter Password
</Button>
</Grid>
</Grid>
)
}
default:
return children
}
}

View File

@ -1,3 +1,15 @@
import * as inMilliseconds from "in-milliseconds"
import {QueryClient} from "@tanstack/react-query"
export const queryClient = new QueryClient()
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchInterval: inMilliseconds.minutes(10),
refetchOnMount: "always",
staleTime: inMilliseconds.minutes(10),
},
},
})

View File

@ -1,76 +1,15 @@
import {ReactElement} from "react"
import {Link as RouterLink} from "react-router-dom"
import {MdLock} from "react-icons/md"
import {Button, Grid, Typography} from "@mui/material"
import {useUser} from "~/hooks"
import {DecryptionPasswordMissingAlert} from "~/components"
export default function WithEncryptionRequired(
Component: any,
): (props: any) => ReactElement {
return (props: any): ReactElement => {
const user = useUser()
if (!user.encryptedPassword) {
return (
<Grid container spacing={4}>
<Grid item>
<Typography variant="h6" component="h2">
Encryption required
</Typography>
</Grid>
<Grid item>
<Typography>
To continue, you need to enable encryption.
</Typography>
</Grid>
<Grid item>
<Button
component={RouterLink}
to="/complete-account?setup=true"
startIcon={<MdLock />}
>
Setup encryption
</Button>
</Grid>
</Grid>
<DecryptionPasswordMissingAlert>
<Component {...props} />
</DecryptionPasswordMissingAlert>
)
}
if (!user.isDecrypted) {
return (
<Grid
container
spacing={4}
direction="column"
alignItems="center"
>
<Grid item>
<Typography variant="h6" component="h2">
Encryption required
</Typography>
</Grid>
<Grid item>
<Typography>
To continue, please enter your password to decrypt
your data.
</Typography>
</Grid>
<Grid item>
<Button
variant="contained"
component={RouterLink}
to="/enter-password"
startIcon={<MdLock />}
>
Enter Password
</Button>
</Grid>
</Grid>
)
}
return <Component {...props} />
}
}

View File

@ -6,3 +6,5 @@ export * from "./use-user"
export {default as useUser} from "./use-user"
export * from "./use-system-preferred-theme"
export {default as useSystemPreferredTheme} from "./use-system-preferred-theme"
export * from "./use-ui-state"
export {default as useUIState} from "./use-ui-state"

14
src/hooks/use-ui-state.ts Normal file
View File

@ -0,0 +1,14 @@
import {Dispatch, SetStateAction, useState} from "react"
import {useUpdateEffect} from "react-use"
export default function useUIState<T>(
outerValue: T,
): [T, Dispatch<SetStateAction<T>>] {
const [value, setValue] = useState<T>(outerValue)
useUpdateEffect(() => {
setValue(outerValue)
}, [outerValue])
return [value, setValue]
}

View File

@ -0,0 +1,97 @@
import {useParams} from "react-router"
import {ReactElement, useContext} from "react"
import {Grid, Typography} from "@mui/material"
import {AliasTypeIndicator, DecryptionPasswordMissingAlert} from "~/components"
import {Alias, DecryptedAlias} from "~/server-types"
import {useUIState} from "~/hooks"
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
import ChangeAliasActivationStatusSwitch from "~/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch"
export interface AliasDetailsProps {
alias: Alias | DecryptedAlias
}
export default function AliasDetails({
alias: aliasValue,
}: AliasDetailsProps): ReactElement {
const params = useParams()
const {encryptionStatus} = useContext(AuthContext)
const address = atob(params.addressInBase64 as string)
const [aliasUIState, setAliasUIState] = useUIState<Alias | DecryptedAlias>(
aliasValue,
)
return (
<Grid container spacing={4}>
<Grid item>
<Grid container spacing={1} direction="row" alignItems="center">
<Grid item>
<AliasTypeIndicator type={aliasUIState.type} />
</Grid>
<Grid item>
<Typography variant="subtitle1">{address}</Typography>
</Grid>
<Grid item>
<ChangeAliasActivationStatusSwitch
id={aliasUIState.id}
isActive={aliasUIState.isActive}
onChanged={setAliasUIState}
/>
</Grid>
</Grid>
</Grid>
<Grid item width="100%">
<Grid container direction="column" spacing={4}>
<Grid item>
<Typography variant="h6" component="h3">
Notes
</Typography>
</Grid>
<Grid item>
{encryptionStatus === EncryptionStatus.Available ? (
<AliasNotesForm
id={aliasUIState.id}
notes={(aliasUIState as DecryptedAlias).notes}
onChanged={setAliasUIState}
/>
) : (
<DecryptionPasswordMissingAlert />
)}
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container spacing={4}>
<Grid item>
<Typography variant="h6" component="h3">
Settings
</Typography>
</Grid>
<Grid item>
<Typography variant="body1">
These settings apply to this alias only. You can
either set a value manually or refer to your
defaults settings. Note that this does change in
behavior. When you set a value to refer to your
default setting, the alias will always use the
latest value. So when you change your default
setting, the alias will automatically use the new
value.
</Typography>
</Grid>
<Grid item>
<AliasPreferencesForm
alias={aliasUIState}
onChanged={setAliasUIState}
/>
</Grid>
</Grid>
</Grid>
</Grid>
)
}

View File

@ -4,6 +4,7 @@ import {AxiosError} from "axios"
import {ReactElement, useContext} from "react"
import {RiLinkM, RiStickyNoteFill} from "react-icons/ri"
import {FieldArray, FormikProvider, useFormik} from "formik"
import update from "immutability-helper"
import {useMutation} from "@tanstack/react-query"
import {
@ -22,15 +23,17 @@ import {
} from "@mui/material"
import {URL_REGEX} from "~/constants/values"
import {parseFastAPIError, whenEnterPressed} from "~/utils"
import {decryptAliasNotes, parseFastAPIError, whenEnterPressed} from "~/utils"
import {BackupImage, ErrorSnack, SuccessSnack} from "~/components"
import {Alias, AliasNote} from "~/server-types"
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
import {UpdateAliasData, updateAlias} from "~/apis"
import AuthContext from "~/AuthContext/AuthContext"
export interface AliasNotesFormProps {
id: string
notes: AliasNote
onChanged: (alias: DecryptedAlias) => void
}
interface Form {
@ -65,13 +68,19 @@ const getDomain = (url: string): string => {
export default function AliasNotesForm({
id,
notes,
onChanged,
}: AliasNotesFormProps): ReactElement {
const {_encryptUsingMasterPassword} = useContext(AuthContext)
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} =
useContext(AuthContext)
const {mutateAsync, isSuccess} = useMutation<
Alias,
AxiosError,
UpdateAliasData
>(values => updateAlias(id, values))
>(values => updateAlias(id, values), {
onSuccess: newAlias => {
onChanged(decryptAliasNotes(newAlias, _decryptUsingMasterPassword))
},
})
const formik = useFormik<Form>({
validationSchema: SCHEMA,
initialValues: {
@ -80,14 +89,17 @@ export default function AliasNotesForm({
},
onSubmit: async (values, {setErrors}) => {
try {
const newNotes = {
...notes,
const newNotes = update(notes, {
data: {
...notes.data,
personalNotes: values.personalNotes,
websites: values.websites,
personalNotes: {
$set: values.personalNotes,
},
}
websites: {
$set: values.websites,
},
},
})
const data = _encryptUsingMasterPassword(
JSON.stringify(newNotes),
)

View File

@ -30,6 +30,8 @@ import SelectField from "~/route-widgets/SettingsRoute/SelectField"
export interface AliasPreferencesFormProps {
alias: Alias | DecryptedAlias
onChanged: (newAlias: Alias | DecryptedAlias) => void
}
interface Form {
@ -56,12 +58,15 @@ const SCHEMA = yup.object().shape({
export default function AliasPreferencesForm({
alias,
onChanged,
}: AliasPreferencesFormProps): ReactElement {
const {mutateAsync, isSuccess} = useMutation<
Alias,
AxiosError,
UpdateAliasData
>(data => updateAlias(alias.id, data))
>(data => updateAlias(alias.id, data), {
onSuccess: onChanged,
})
const formik = useFormik<Form>({
enableReinitialize: true,
initialValues: {

View File

@ -1,19 +1,20 @@
import {ReactElement, useEffect, useState} from "react"
import {ReactElement, useContext, useEffect, useState} from "react"
import {AxiosError} from "axios"
import {Switch} from "@mui/material"
import {useMutation} from "@tanstack/react-query"
import {Alias} from "~/server-types"
import {Alias, DecryptedAlias} from "~/server-types"
import {UpdateAliasData, updateAlias} from "~/apis"
import {parseFastAPIError} from "~/utils"
import {decryptAliasNotes, parseFastAPIError} from "~/utils"
import {ErrorSnack, SuccessSnack} from "~/components"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
export interface ChangeAliasActivationStatusSwitchProps {
id: string
isActive: boolean
onChanged: () => void
onChanged: (alias: Alias | DecryptedAlias) => void
}
export default function ChangeAliasActivationStatusSwitch({
@ -21,6 +22,9 @@ export default function ChangeAliasActivationStatusSwitch({
isActive,
onChanged,
}: ChangeAliasActivationStatusSwitchProps): ReactElement {
const {_decryptUsingMasterPassword, encryptionStatus} =
useContext(AuthContext)
const [isActiveUIState, setIsActiveUIState] = useState<boolean>(true)
const [successMessage, setSuccessMessage] = useState<string>("")
@ -31,7 +35,15 @@ export default function ChangeAliasActivationStatusSwitch({
AxiosError,
UpdateAliasData
>(values => updateAlias(id, values), {
onSuccess: onChanged,
onSuccess: newAlias => {
if (encryptionStatus === EncryptionStatus.Available) {
onChanged(
decryptAliasNotes(newAlias, _decryptUsingMasterPassword),
)
} else {
onChanged(newAlias)
}
},
onError: error =>
setErrorMessage(parseFastAPIError(error).detail as string),
})

View File

@ -3,27 +3,24 @@ import {useParams} from "react-router-dom"
import {AxiosError} from "axios"
import {useQuery} from "@tanstack/react-query"
import {Grid, Typography} from "@mui/material"
import {getAlias} from "~/apis"
import {Alias, DecryptedAlias} from "~/server-types"
import {AliasTypeIndicator, QueryResult, SimplePage} from "~/components"
import {QueryResult, SimplePage} from "~/components"
import {decryptAliasNotes} from "~/utils"
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm"
import AuthContext from "~/AuthContext/AuthContext"
import ChangeAliasActivationStatusSwitch from "~/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch"
import DecryptionPasswordMissingAlert from "~/components/DecryptionPasswordMissingAlert"
import AliasDetails from "~/route-widgets/AliasDetailRoute/AliasDetails"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
export default function AliasDetailRoute(): ReactElement {
const params = useParams()
const {user, _decryptUsingMasterPassword} = useContext(AuthContext)
const address = atob(params.addressInBase64 as string)
const {user, _decryptUsingMasterPassword, encryptionStatus} =
useContext(AuthContext)
const query = useQuery<Alias | DecryptedAlias, AxiosError>(
["get_alias", params.addressInBase64],
async () => {
if (user?.encryptedPassword) {
if (encryptionStatus === EncryptionStatus.Available) {
return decryptAliasNotes(
await getAlias(address),
_decryptUsingMasterPassword,
@ -37,81 +34,7 @@ export default function AliasDetailRoute(): ReactElement {
return (
<SimplePage title="Alias Details">
<QueryResult<Alias | DecryptedAlias> query={query}>
{alias => (
<Grid container spacing={4}>
<Grid item>
<Grid
container
spacing={1}
direction="row"
alignItems="center"
>
<Grid item>
<AliasTypeIndicator type={alias.type} />
</Grid>
<Grid item>
<Typography variant="subtitle1">
{address}
</Typography>
</Grid>
<Grid item>
<ChangeAliasActivationStatusSwitch
id={alias.id}
isActive={alias.isActive}
onChanged={query.refetch}
/>
</Grid>
</Grid>
</Grid>
<Grid item width="100%">
<Grid container direction="column" spacing={4}>
<Grid item>
<Typography variant="h6" component="h3">
Notes
</Typography>
</Grid>
<Grid item>
{user?.encryptedPassword &&
(alias as DecryptedAlias).notes ? (
<AliasNotesForm
id={alias.id}
notes={
(alias as DecryptedAlias).notes
}
/>
) : (
<DecryptionPasswordMissingAlert />
)}
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container spacing={4}>
<Grid item>
<Typography variant="h6" component="h3">
Settings
</Typography>
</Grid>
<Grid item>
<Typography variant="body1">
These settings apply to this alias only.
You can either set a value manually or
refer to your defaults settings. Note
that this does change in behavior. When
you set a value to refer to your default
setting, the alias will always use the
latest value. So when you change your
default setting, the alias will
automatically use the new value.
</Typography>
</Grid>
<Grid item>
<AliasPreferencesForm alias={alias} />
</Grid>
</Grid>
</Grid>
</Grid>
)}
{alias => <AliasDetails alias={alias} />}
</QueryResult>
</SimplePage>
)

View File

@ -1,13 +1,14 @@
import * as yup from "yup"
import {ReactElement, useContext} from "react"
import {useNavigate} from "react-router-dom"
import {useLocation, useNavigate} from "react-router-dom"
import {useFormik} from "formik"
import {MdLock} from "react-icons/md"
import {InputAdornment} from "@mui/material"
import {buildEncryptionPassword} from "~/utils"
import {useUser} from "~/hooks"
import {PasswordField, SimpleForm} from "~/components"
import {InputAdornment} from "@mui/material"
import {MdLock} from "react-icons/md"
import AuthContext from "~/AuthContext/AuthContext"
interface Form {
@ -20,6 +21,7 @@ const schema = yup.object().shape({
export default function EnterDecryptionPassword(): ReactElement {
const navigate = useNavigate()
const location = useLocation()
const user = useUser()
const {_setDecryptionPassword} = useContext(AuthContext)
@ -37,7 +39,9 @@ export default function EnterDecryptionPassword(): ReactElement {
if (!_setDecryptionPassword(decryptionPassword)) {
setErrors({password: "Password is invalid."})
} else {
navigate("/")
const nextUrl =
new URLSearchParams(location.search).get("next") || "/"
navigate(nextUrl)
}
},
})