mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
improvements
This commit is contained in:
parent
64a8ae3096
commit
c6634dc740
@ -23,6 +23,8 @@
|
|||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"group-array": "^1.0.0",
|
"group-array": "^1.0.0",
|
||||||
|
"immutability-helper": "^3.1.1",
|
||||||
|
"in-milliseconds": "^1.2.0",
|
||||||
"in-seconds": "^1.2.0",
|
"in-seconds": "^1.2.0",
|
||||||
"openpgp": "^5.5.0",
|
"openpgp": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -2,11 +2,18 @@ import {createContext} from "react"
|
|||||||
|
|
||||||
import {ServerUser, User} from "~/server-types"
|
import {ServerUser, User} from "~/server-types"
|
||||||
|
|
||||||
|
export enum EncryptionStatus {
|
||||||
|
Unavailable = "Unavailable",
|
||||||
|
PasswordRequired = "PasswordRequired",
|
||||||
|
Available = "Available",
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthContextTypeBase {
|
interface AuthContextTypeBase {
|
||||||
user: ServerUser | User | null
|
user: ServerUser | User | null
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
login: (user: ServerUser | User) => void
|
login: (user: ServerUser | User) => void
|
||||||
logout: () => void
|
logout: () => void
|
||||||
|
encryptionStatus: EncryptionStatus
|
||||||
_decryptUsingMasterPassword: (content: string) => string
|
_decryptUsingMasterPassword: (content: string) => string
|
||||||
_encryptUsingMasterPassword: (content: string) => string
|
_encryptUsingMasterPassword: (content: string) => string
|
||||||
_decryptUsingPrivateKey: (message: string) => Promise<string>
|
_decryptUsingPrivateKey: (message: string) => Promise<string>
|
||||||
@ -31,6 +38,7 @@ export type AuthContextType =
|
|||||||
const AuthContext = createContext<AuthContextType>({
|
const AuthContext = createContext<AuthContextType>({
|
||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
|
encryptionStatus: EncryptionStatus.Unavailable,
|
||||||
login: () => {
|
login: () => {
|
||||||
throw new Error("login() not implemented")
|
throw new Error("login() not implemented")
|
||||||
},
|
},
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import {client} from "~/constants/axios-client"
|
import {client} from "~/constants/axios-client"
|
||||||
import {decryptString, encryptString} from "~/utils"
|
import {decryptString, encryptString} from "~/utils"
|
||||||
|
|
||||||
import AuthContext, {AuthContextType} from "./AuthContext"
|
import AuthContext, {AuthContextType, EncryptionStatus} from "./AuthContext"
|
||||||
|
|
||||||
export interface AuthContextProviderProps {
|
export interface AuthContextProviderProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@ -134,6 +134,21 @@ export default function AuthContextProvider({
|
|||||||
() => ({
|
() => ({
|
||||||
user: user ?? null,
|
user: user ?? null,
|
||||||
login: setUser,
|
login: setUser,
|
||||||
|
encryptionStatus: (() => {
|
||||||
|
if (!user) {
|
||||||
|
return EncryptionStatus.Unavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.encryptedPassword) {
|
||||||
|
return EncryptionStatus.Unavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isDecrypted) {
|
||||||
|
return EncryptionStatus.Available
|
||||||
|
}
|
||||||
|
|
||||||
|
return EncryptionStatus.PasswordRequired
|
||||||
|
})(),
|
||||||
logout,
|
logout,
|
||||||
isAuthenticated: user !== null,
|
isAuthenticated: user !== null,
|
||||||
_encryptUsingMasterPassword: encryptUsingMasterPassword,
|
_encryptUsingMasterPassword: encryptUsingMasterPassword,
|
||||||
|
@ -1,31 +1,85 @@
|
|||||||
import {ReactElement, useContext} from "react"
|
import {useContext} from "react"
|
||||||
import {MdLock} from "react-icons/md"
|
import {MdLock} from "react-icons/md"
|
||||||
import {Link as RouterLink} from "react-router-dom"
|
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"
|
import LockNavigationContext from "~/LockNavigationContext/LockNavigationContext"
|
||||||
|
|
||||||
export default function DecryptionPasswordMissingAlert(): ReactElement {
|
export interface WithEncryptionRequiredProps {
|
||||||
const {handleAnchorClick} = useContext(LockNavigationContext)
|
children?: JSX.Element
|
||||||
|
}
|
||||||
return (
|
|
||||||
<Grid container spacing={2} direction="column" alignItems="center">
|
export default function DecryptionPasswordMissingAlert({
|
||||||
<Grid item>
|
children = <></>,
|
||||||
<Alert severity="warning">
|
}: WithEncryptionRequiredProps): JSX.Element {
|
||||||
Your decryption password is required to view this section.
|
const {handleAnchorClick} = useContext(LockNavigationContext)
|
||||||
</Alert>
|
const {encryptionStatus} = useContext(AuthContext)
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
switch (encryptionStatus) {
|
||||||
<Button
|
case EncryptionStatus.Unavailable: {
|
||||||
startIcon={<MdLock />}
|
return (
|
||||||
component={RouterLink}
|
<Grid container spacing={4}>
|
||||||
to="/enter-password"
|
<Grid item>
|
||||||
onClick={handleAnchorClick}
|
<Typography variant="h6" component="h2">
|
||||||
>
|
Encryption required
|
||||||
Enter password
|
</Typography>
|
||||||
</Button>
|
</Grid>
|
||||||
</Grid>
|
<Grid item>
|
||||||
</Grid>
|
<Typography>
|
||||||
)
|
You need to set up encryption to use this feature.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
component={RouterLink}
|
||||||
|
to="/complete-account?setup=true"
|
||||||
|
startIcon={<MdLock />}
|
||||||
|
onClick={handleAnchorClick}
|
||||||
|
>
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,15 @@
|
|||||||
|
import * as inMilliseconds from "in-milliseconds"
|
||||||
|
|
||||||
import {QueryClient} from "@tanstack/react-query"
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -1,76 +1,15 @@
|
|||||||
import {ReactElement} from "react"
|
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 {DecryptionPasswordMissingAlert} from "~/components"
|
||||||
|
|
||||||
import {useUser} from "~/hooks"
|
|
||||||
|
|
||||||
export default function WithEncryptionRequired(
|
export default function WithEncryptionRequired(
|
||||||
Component: any,
|
Component: any,
|
||||||
): (props: any) => ReactElement {
|
): (props: any) => ReactElement {
|
||||||
return (props: any): ReactElement => {
|
return (props: any): ReactElement => {
|
||||||
const user = useUser()
|
return (
|
||||||
|
<DecryptionPasswordMissingAlert>
|
||||||
if (!user.encryptedPassword) {
|
<Component {...props} />
|
||||||
return (
|
</DecryptionPasswordMissingAlert>
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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} />
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,3 +6,5 @@ export * from "./use-user"
|
|||||||
export {default as useUser} from "./use-user"
|
export {default as useUser} from "./use-user"
|
||||||
export * from "./use-system-preferred-theme"
|
export * from "./use-system-preferred-theme"
|
||||||
export {default as useSystemPreferredTheme} 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
14
src/hooks/use-ui-state.ts
Normal 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]
|
||||||
|
}
|
97
src/route-widgets/AliasDetailRoute/AliasDetails.tsx
Normal file
97
src/route-widgets/AliasDetailRoute/AliasDetails.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -4,6 +4,7 @@ import {AxiosError} from "axios"
|
|||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext} from "react"
|
||||||
import {RiLinkM, RiStickyNoteFill} from "react-icons/ri"
|
import {RiLinkM, RiStickyNoteFill} from "react-icons/ri"
|
||||||
import {FieldArray, FormikProvider, useFormik} from "formik"
|
import {FieldArray, FormikProvider, useFormik} from "formik"
|
||||||
|
import update from "immutability-helper"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import {
|
import {
|
||||||
@ -22,15 +23,17 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
|
||||||
import {URL_REGEX} from "~/constants/values"
|
import {URL_REGEX} from "~/constants/values"
|
||||||
import {parseFastAPIError, whenEnterPressed} from "~/utils"
|
import {decryptAliasNotes, parseFastAPIError, whenEnterPressed} from "~/utils"
|
||||||
import {BackupImage, ErrorSnack, SuccessSnack} from "~/components"
|
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 {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
|
|
||||||
export interface AliasNotesFormProps {
|
export interface AliasNotesFormProps {
|
||||||
id: string
|
id: string
|
||||||
notes: AliasNote
|
notes: AliasNote
|
||||||
|
|
||||||
|
onChanged: (alias: DecryptedAlias) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Form {
|
interface Form {
|
||||||
@ -65,13 +68,19 @@ const getDomain = (url: string): string => {
|
|||||||
export default function AliasNotesForm({
|
export default function AliasNotesForm({
|
||||||
id,
|
id,
|
||||||
notes,
|
notes,
|
||||||
|
onChanged,
|
||||||
}: AliasNotesFormProps): ReactElement {
|
}: AliasNotesFormProps): ReactElement {
|
||||||
const {_encryptUsingMasterPassword} = useContext(AuthContext)
|
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} =
|
||||||
|
useContext(AuthContext)
|
||||||
const {mutateAsync, isSuccess} = useMutation<
|
const {mutateAsync, isSuccess} = useMutation<
|
||||||
Alias,
|
Alias,
|
||||||
AxiosError,
|
AxiosError,
|
||||||
UpdateAliasData
|
UpdateAliasData
|
||||||
>(values => updateAlias(id, values))
|
>(values => updateAlias(id, values), {
|
||||||
|
onSuccess: newAlias => {
|
||||||
|
onChanged(decryptAliasNotes(newAlias, _decryptUsingMasterPassword))
|
||||||
|
},
|
||||||
|
})
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: SCHEMA,
|
validationSchema: SCHEMA,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -80,14 +89,17 @@ export default function AliasNotesForm({
|
|||||||
},
|
},
|
||||||
onSubmit: async (values, {setErrors}) => {
|
onSubmit: async (values, {setErrors}) => {
|
||||||
try {
|
try {
|
||||||
const newNotes = {
|
const newNotes = update(notes, {
|
||||||
...notes,
|
|
||||||
data: {
|
data: {
|
||||||
...notes.data,
|
personalNotes: {
|
||||||
personalNotes: values.personalNotes,
|
$set: values.personalNotes,
|
||||||
websites: values.websites,
|
},
|
||||||
|
websites: {
|
||||||
|
$set: values.websites,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
const data = _encryptUsingMasterPassword(
|
const data = _encryptUsingMasterPassword(
|
||||||
JSON.stringify(newNotes),
|
JSON.stringify(newNotes),
|
||||||
)
|
)
|
||||||
|
@ -30,6 +30,8 @@ import SelectField from "~/route-widgets/SettingsRoute/SelectField"
|
|||||||
|
|
||||||
export interface AliasPreferencesFormProps {
|
export interface AliasPreferencesFormProps {
|
||||||
alias: Alias | DecryptedAlias
|
alias: Alias | DecryptedAlias
|
||||||
|
|
||||||
|
onChanged: (newAlias: Alias | DecryptedAlias) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Form {
|
interface Form {
|
||||||
@ -56,12 +58,15 @@ const SCHEMA = yup.object().shape({
|
|||||||
|
|
||||||
export default function AliasPreferencesForm({
|
export default function AliasPreferencesForm({
|
||||||
alias,
|
alias,
|
||||||
|
onChanged,
|
||||||
}: AliasPreferencesFormProps): ReactElement {
|
}: AliasPreferencesFormProps): ReactElement {
|
||||||
const {mutateAsync, isSuccess} = useMutation<
|
const {mutateAsync, isSuccess} = useMutation<
|
||||||
Alias,
|
Alias,
|
||||||
AxiosError,
|
AxiosError,
|
||||||
UpdateAliasData
|
UpdateAliasData
|
||||||
>(data => updateAlias(alias.id, data))
|
>(data => updateAlias(alias.id, data), {
|
||||||
|
onSuccess: onChanged,
|
||||||
|
})
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import {ReactElement, useEffect, useState} from "react"
|
import {ReactElement, useContext, useEffect, useState} from "react"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
|
||||||
import {Switch} from "@mui/material"
|
import {Switch} from "@mui/material"
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
|
|
||||||
import {Alias} from "~/server-types"
|
import {Alias, DecryptedAlias} from "~/server-types"
|
||||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {decryptAliasNotes, parseFastAPIError} from "~/utils"
|
||||||
import {ErrorSnack, SuccessSnack} from "~/components"
|
import {ErrorSnack, SuccessSnack} from "~/components"
|
||||||
|
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
||||||
|
|
||||||
export interface ChangeAliasActivationStatusSwitchProps {
|
export interface ChangeAliasActivationStatusSwitchProps {
|
||||||
id: string
|
id: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
|
||||||
onChanged: () => void
|
onChanged: (alias: Alias | DecryptedAlias) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangeAliasActivationStatusSwitch({
|
export default function ChangeAliasActivationStatusSwitch({
|
||||||
@ -21,6 +22,9 @@ export default function ChangeAliasActivationStatusSwitch({
|
|||||||
isActive,
|
isActive,
|
||||||
onChanged,
|
onChanged,
|
||||||
}: ChangeAliasActivationStatusSwitchProps): ReactElement {
|
}: ChangeAliasActivationStatusSwitchProps): ReactElement {
|
||||||
|
const {_decryptUsingMasterPassword, encryptionStatus} =
|
||||||
|
useContext(AuthContext)
|
||||||
|
|
||||||
const [isActiveUIState, setIsActiveUIState] = useState<boolean>(true)
|
const [isActiveUIState, setIsActiveUIState] = useState<boolean>(true)
|
||||||
|
|
||||||
const [successMessage, setSuccessMessage] = useState<string>("")
|
const [successMessage, setSuccessMessage] = useState<string>("")
|
||||||
@ -31,7 +35,15 @@ export default function ChangeAliasActivationStatusSwitch({
|
|||||||
AxiosError,
|
AxiosError,
|
||||||
UpdateAliasData
|
UpdateAliasData
|
||||||
>(values => updateAlias(id, values), {
|
>(values => updateAlias(id, values), {
|
||||||
onSuccess: onChanged,
|
onSuccess: newAlias => {
|
||||||
|
if (encryptionStatus === EncryptionStatus.Available) {
|
||||||
|
onChanged(
|
||||||
|
decryptAliasNotes(newAlias, _decryptUsingMasterPassword),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onChanged(newAlias)
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: error =>
|
onError: error =>
|
||||||
setErrorMessage(parseFastAPIError(error).detail as string),
|
setErrorMessage(parseFastAPIError(error).detail as string),
|
||||||
})
|
})
|
||||||
|
@ -3,27 +3,24 @@ import {useParams} from "react-router-dom"
|
|||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
import {Grid, Typography} from "@mui/material"
|
|
||||||
|
|
||||||
import {getAlias} from "~/apis"
|
import {getAlias} from "~/apis"
|
||||||
import {Alias, DecryptedAlias} from "~/server-types"
|
import {Alias, DecryptedAlias} from "~/server-types"
|
||||||
import {AliasTypeIndicator, QueryResult, SimplePage} from "~/components"
|
import {QueryResult, SimplePage} from "~/components"
|
||||||
import {decryptAliasNotes} from "~/utils"
|
import {decryptAliasNotes} from "~/utils"
|
||||||
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
|
import AliasDetails from "~/route-widgets/AliasDetailRoute/AliasDetails"
|
||||||
import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm"
|
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
|
||||||
import ChangeAliasActivationStatusSwitch from "~/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch"
|
|
||||||
import DecryptionPasswordMissingAlert from "~/components/DecryptionPasswordMissingAlert"
|
|
||||||
|
|
||||||
export default function AliasDetailRoute(): ReactElement {
|
export default function AliasDetailRoute(): ReactElement {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const {user, _decryptUsingMasterPassword} = useContext(AuthContext)
|
|
||||||
const address = atob(params.addressInBase64 as string)
|
const address = atob(params.addressInBase64 as string)
|
||||||
|
const {user, _decryptUsingMasterPassword, encryptionStatus} =
|
||||||
|
useContext(AuthContext)
|
||||||
|
|
||||||
const query = useQuery<Alias | DecryptedAlias, AxiosError>(
|
const query = useQuery<Alias | DecryptedAlias, AxiosError>(
|
||||||
["get_alias", params.addressInBase64],
|
["get_alias", params.addressInBase64],
|
||||||
async () => {
|
async () => {
|
||||||
if (user?.encryptedPassword) {
|
if (encryptionStatus === EncryptionStatus.Available) {
|
||||||
return decryptAliasNotes(
|
return decryptAliasNotes(
|
||||||
await getAlias(address),
|
await getAlias(address),
|
||||||
_decryptUsingMasterPassword,
|
_decryptUsingMasterPassword,
|
||||||
@ -37,81 +34,7 @@ export default function AliasDetailRoute(): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<SimplePage title="Alias Details">
|
<SimplePage title="Alias Details">
|
||||||
<QueryResult<Alias | DecryptedAlias> query={query}>
|
<QueryResult<Alias | DecryptedAlias> query={query}>
|
||||||
{alias => (
|
{alias => <AliasDetails alias={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>
|
|
||||||
)}
|
|
||||||
</QueryResult>
|
</QueryResult>
|
||||||
</SimplePage>
|
</SimplePage>
|
||||||
)
|
)
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import * as yup from "yup"
|
import * as yup from "yup"
|
||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext} from "react"
|
||||||
import {useNavigate} from "react-router-dom"
|
import {useLocation, useNavigate} from "react-router-dom"
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
|
import {MdLock} from "react-icons/md"
|
||||||
|
|
||||||
|
import {InputAdornment} from "@mui/material"
|
||||||
|
|
||||||
import {buildEncryptionPassword} from "~/utils"
|
import {buildEncryptionPassword} from "~/utils"
|
||||||
import {useUser} from "~/hooks"
|
import {useUser} from "~/hooks"
|
||||||
import {PasswordField, SimpleForm} from "~/components"
|
import {PasswordField, SimpleForm} from "~/components"
|
||||||
import {InputAdornment} from "@mui/material"
|
|
||||||
import {MdLock} from "react-icons/md"
|
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
|
|
||||||
interface Form {
|
interface Form {
|
||||||
@ -20,6 +21,7 @@ const schema = yup.object().shape({
|
|||||||
|
|
||||||
export default function EnterDecryptionPassword(): ReactElement {
|
export default function EnterDecryptionPassword(): ReactElement {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const {_setDecryptionPassword} = useContext(AuthContext)
|
const {_setDecryptionPassword} = useContext(AuthContext)
|
||||||
|
|
||||||
@ -37,7 +39,9 @@ export default function EnterDecryptionPassword(): ReactElement {
|
|||||||
if (!_setDecryptionPassword(decryptionPassword)) {
|
if (!_setDecryptionPassword(decryptionPassword)) {
|
||||||
setErrors({password: "Password is invalid."})
|
setErrors({password: "Password is invalid."})
|
||||||
} else {
|
} else {
|
||||||
navigate("/")
|
const nextUrl =
|
||||||
|
new URLSearchParams(location.search).get("next") || "/"
|
||||||
|
navigate(nextUrl)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user