mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
improving AuthContextProvider.tsx
This commit is contained in:
parent
cf21360509
commit
caa1a108d8
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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!,
|
||||||
|
78
src/AuthContext/use-user.ts
Normal file
78
src/AuthContext/use-user.ts
Normal 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])
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user