improving AuthContextProvider.tsx

This commit is contained in:
Myzel394 2022-12-12 09:17:05 +01:00
parent cf21360509
commit caa1a108d8
4 changed files with 131 additions and 93 deletions

View File

@ -22,6 +22,7 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"deep-equal": "^2.0.5", "deep-equal": "^2.0.5",
"fast-hash-code": "^2.1.0",
"formik": "^2.2.9", "formik": "^2.2.9",
"group-array": "^1.0.0", "group-array": "^1.0.0",
"i18next": "^22.0.4", "i18next": "^22.0.4",

View File

@ -1,18 +1,15 @@
import {ReactElement, ReactNode, useCallback, useEffect} from "react" import {ReactElement, ReactNode, useCallback} from "react"
import {AxiosError} from "axios"
import {decrypt, readMessage, readPrivateKey} from "openpgp"
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import AuthContext from "./AuthContext" import fastHashCode from "fast-hash-code";
import useExtensionHandler from "~/AuthContext/use-extension-handler"
import useMasterPassword from "~/AuthContext/use-master-password"
import {AuthenticationDetails, ServerUser, User} from "~/server-types" import {ServerUser, User} from "~/server-types"
import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis"
import {client} from "~/constants/axios-client" import AuthContext from "./AuthContext"
import {useMutation, useQuery} from "@tanstack/react-query" import PasswordShareConfirmationDialog from "./PasswordShareConfirmationDialog"
import PasswordShareConfirmationDialog from "~/AuthContext/PasswordShareConfirmationDialog" import useContextValue from "./use-context-value"
import useContextValue from "~/AuthContext/use-context-value" import useExtensionHandler from "./use-extension-handler"
import useMasterPassword from "./use-master-password"
import useUser from "./use-user";
export interface AuthContextProviderProps { export interface AuthContextProviderProps {
children: ReactNode children: ReactNode
@ -23,48 +20,23 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
"_global-context-auth-user", "_global-context-auth-user",
null, null,
) )
const { const {
encryptUsingMasterPassword, encryptUsingMasterPassword,
decryptUsingMasterPassword, decryptUsingMasterPassword,
decryptUsingPrivateKey,
setDecryptionPassword, setDecryptionPassword,
_masterPassword, _masterPassword,
logout: logoutMasterPassword, logout: logoutMasterPassword,
} = useMasterPassword(user?.encryptedPassword || null) } = useMasterPassword(user || null)
const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler( const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler(
_masterPassword!, _masterPassword!,
user as User, user as User,
) )
const logout = useCallback(() => { const logout = useCallback(() => {
logoutMasterPassword() logoutMasterPassword()
setUser(null)
}, [logoutMasterPassword]) }, [logoutMasterPassword])
const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, {
onError: () => logout(),
})
const decryptUsingPrivateKey = useCallback(
async (message: string): Promise<string> => {
if (!user) {
throw new Error("User not set.")
}
if (!user.isDecrypted) {
throw new Error("User is not decrypted.")
}
return (
await decrypt({
message: await readMessage({
armoredMessage: message,
}),
decryptionKeys: await readPrivateKey({
armoredKey: user.notes.privateKey,
}),
})
).data.toString()
},
[user],
)
const contextValue = useContextValue({ const contextValue = useContextValue({
decryptUsingPrivateKey, decryptUsingPrivateKey,
@ -76,55 +48,14 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
user: user || null, user: user || null,
}) })
useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, { useUser({
refetchOnWindowFocus: "always", logout,
refetchOnReconnect: "always", decryptUsingMasterPassword,
retry: 2, user: user || null,
enabled: user !== null, updateUser: setUser,
masterPasswordHash: _masterPassword ? fastHashCode(_masterPassword).toString() : null,
}) })
// Decrypt user notes
useEffect(() => {
if (user && !user.isDecrypted && user.encryptedPassword) {
const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!))
setUser(
prevUser =>
({
...(prevUser || {}),
notes: note,
isDecrypted: true,
} as User),
)
}
}, [user, decryptUsingMasterPassword])
// Refresh token and logout user if needed
useEffect(() => {
const interceptor = client.interceptors.response.use(
response => response,
async (error: AxiosError) => {
if (error.isAxiosError) {
if (error.response?.status === 401) {
// Check if error comes from refreshing the token.
// If yes, the user has been logged out completely.
const request: XMLHttpRequest = error.request
if (request.responseURL === REFRESH_TOKEN_URL) {
await logout()
} else {
await refresh()
}
}
}
throw error
},
)
return () => client.interceptors.response.eject(interceptor)
}, [logout, refresh])
return ( return (
<> <>
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider> <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>

View File

@ -1,11 +1,14 @@
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import {useCallback, useMemo} from "react" import {useCallback, useMemo} from "react"
import {decrypt, readMessage, readPrivateKey} from "openpgp";
import {decryptString, encryptString} from "~/utils" import {decryptString, encryptString} from "~/utils"
import {ServerUser, User} from "~/server-types";
export interface UseMasterPasswordResult { export interface UseMasterPasswordResult {
encryptUsingMasterPassword: (content: string) => string encryptUsingMasterPassword: (content: string) => string
decryptUsingMasterPassword: (content: string) => string decryptUsingMasterPassword: (content: string) => string
decryptUsingPrivateKey: (message: string) => Promise<string>
setDecryptionPassword: (password: string) => void setDecryptionPassword: (password: string) => void
logout: () => void logout: () => void
@ -14,7 +17,7 @@ export interface UseMasterPasswordResult {
} }
export default function useMasterPassword( export default function useMasterPassword(
encryptedPassword: string | null, user: User | ServerUser | null,
): UseMasterPasswordResult { ): UseMasterPasswordResult {
const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>( const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>(
"_global-context-auth-decryption-password", "_global-context-auth-decryption-password",
@ -22,12 +25,12 @@ export default function useMasterPassword(
) )
const masterPassword = useMemo<string | null>(() => { const masterPassword = useMemo<string | null>(() => {
if (decryptionPassword === null || !encryptedPassword) { if (decryptionPassword === null || !user?.encryptedPassword) {
return null return null
} }
return decryptString(encryptedPassword, decryptionPassword!) return decryptString(user.encryptedPassword, decryptionPassword!)
}, [decryptionPassword, encryptedPassword]) }, [decryptionPassword, user?.encryptedPassword])
const encryptUsingMasterPassword = useCallback( const encryptUsingMasterPassword = useCallback(
(content: string) => { (content: string) => {
@ -51,6 +54,30 @@ export default function useMasterPassword(
[masterPassword], [masterPassword],
) )
const decryptUsingPrivateKey = useCallback(
async (message: string): Promise<string> => {
if (!user) {
throw new Error("User not set.")
}
if (!user.isDecrypted) {
throw new Error("User is not decrypted.")
}
return (
await decrypt({
message: await readMessage({
armoredMessage: message,
}),
decryptionKeys: await readPrivateKey({
armoredKey: user.notes.privateKey,
}),
})
).data.toString()
},
[user],
)
const logout = useCallback(() => { const logout = useCallback(() => {
setDecryptionPassword(null) setDecryptionPassword(null)
}, []) }, [])
@ -58,6 +85,7 @@ export default function useMasterPassword(
return { return {
encryptUsingMasterPassword, encryptUsingMasterPassword,
decryptUsingMasterPassword, decryptUsingMasterPassword,
decryptUsingPrivateKey,
setDecryptionPassword, setDecryptionPassword,
logout, logout,
_masterPassword: masterPassword!, _masterPassword: masterPassword!,

View File

@ -0,0 +1,78 @@
import {Dispatch, SetStateAction, useEffect} from "react";
import {AxiosError} from "axios";
import {useMutation, useQuery} from "@tanstack/react-query";
import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis"
import {AuthenticationDetails, ServerUser, User} from "~/server-types";
import {client} from "~/constants/axios-client";
export interface UseAuthData {
logout: () => void
masterPasswordHash: string | null
decryptUsingMasterPassword: (content: string) => string
user: User | ServerUser | null
updateUser: Dispatch<SetStateAction<User | ServerUser | null | undefined>>
}
export default function useUser({
logout,
masterPasswordHash,
decryptUsingMasterPassword,
user,
updateUser,
}: UseAuthData) {
const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, {
onError: () => logout(),
})
useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, {
refetchOnWindowFocus: "always",
refetchOnReconnect: "always",
retry: 2,
enabled: user !== null,
})
// Decrypt user notes
useEffect(() => {
if (user && !user.isDecrypted && user.encryptedPassword && masterPasswordHash) {
const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!))
updateUser(
prevUser =>
({
...(prevUser || {}),
notes: note,
isDecrypted: true,
} as User),
)
}
}, [user, decryptUsingMasterPassword, updateUser, masterPasswordHash])
// Refresh token and logout user if needed
useEffect(() => {
const interceptor = client.interceptors.response.use(
response => response,
async (error: AxiosError) => {
if (error.isAxiosError) {
if (error.response?.status === 401) {
// Check if error comes from refreshing the token.
// If yes, the user has been logged out completely.
const request: XMLHttpRequest = error.request
if (request.responseURL === REFRESH_TOKEN_URL) {
await logout()
} else {
await refresh()
}
}
}
throw error
},
)
return () => client.interceptors.response.eject(interceptor)
}, [logout, refresh])
}