improved AuthContext

This commit is contained in:
Myzel394 2022-10-22 12:00:08 +02:00
parent 388fa9488c
commit 62faf096bb
8 changed files with 92 additions and 90 deletions

View File

@ -18,6 +18,7 @@
"@tanstack/react-query": "^4.12.0",
"axios": "^1.1.2",
"axios-case-converter": "^0.11.1",
"camelcase-keys": "^8.0.2",
"crypto-js": "^4.1.1",
"date-fns": "^2.29.3",
"formik": "^2.2.9",

View File

@ -5,10 +5,11 @@ import {ServerUser, User} from "~/server-types"
interface AuthContextTypeBase {
user: ServerUser | User | null
isAuthenticated: boolean
login: (user: ServerUser) => Promise<void>
login: (user: ServerUser | User) => void
logout: () => void
_decryptContent: (content: string) => string
_encryptContent: (content: string) => string
_decryptUsingMasterPassword: (content: string) => string
_encryptUsingMasterPassword: (content: string) => string
_decryptUsingPrivateKey: (message: string) => Promise<string>
_setDecryptionPassword: (decryptionPassword: string) => void
_updateUser: (user: ServerUser | User) => void
}
@ -36,12 +37,15 @@ const AuthContext = createContext<AuthContextType>({
logout: () => {
throw new Error("logout() not implemented")
},
_decryptContent: () => {
_decryptUsingMasterPassword: () => {
throw new Error("_decryptContent() not implemented")
},
_encryptContent: () => {
_encryptUsingMasterPassword: () => {
throw new Error("_encryptContent() not implemented")
},
_decryptUsingPrivateKey: () => {
throw new Error("_decryptUsingPrivateKey() not implemented")
},
_setDecryptionPassword: () => {
throw new Error("_setMasterDecryptionPassword() not implemented")
},

View File

@ -1,6 +1,7 @@
import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react"
import {useLocalStorage} from "react-use"
import {AxiosError} from "axios"
import {decrypt, readMessage, readPrivateKey} from "openpgp"
import {useMutation} from "@tanstack/react-query"
@ -23,6 +24,14 @@ export interface AuthContextProviderProps {
export default function AuthContextProvider({
children,
}: AuthContextProviderProps): ReactElement {
const {mutateAsync: refresh} = useMutation<
RefreshTokenResult,
AxiosError,
void
>(refreshToken, {
onError: () => logout(false),
})
const [decryptionPassword, setDecryptionPassword] = useLocalStorage<
string | null
>("_global-context-auth-decryption-password", null)
@ -30,6 +39,7 @@ export default function AuthContextProvider({
"_global-context-auth-user",
null,
)
const masterPassword = useMemo<string | null>(() => {
if (decryptionPassword === null || !user?.encryptedPassword) {
return null
@ -46,7 +56,7 @@ export default function AuthContextProvider({
}
}, [])
const encryptContent = useCallback(
const encryptUsingMasterPassword = useCallback(
(content: string) => {
if (!masterPassword) {
throw new Error("Master password not set.")
@ -57,7 +67,7 @@ export default function AuthContextProvider({
[masterPassword],
)
const decryptContent = useCallback(
const decryptUsingMasterPassword = useCallback(
(content: string) => {
if (!masterPassword) {
throw new Error("Master password not set.")
@ -68,75 +78,68 @@ export default function AuthContextProvider({
[masterPassword],
)
const tryDecryptUserNote = useCallback(
(newUser?: ServerUser): void => {
const userData: ServerUser = newUser ?? user
if (userData?.encryptedNotes && masterPassword) {
const note = JSON.parse(
decryptContent(userData.encryptedNotes!),
)
// @ts-ignore
const newUser: User = {
...user,
notes: note,
isDecrypted: true,
}
setUser(newUser)
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, decryptContent, masterPassword],
[user],
)
const updateUser = useCallback(
(newUser: ServerUser | User) => {
setUser(newUser)
tryDecryptUserNote()
},
[user, tryDecryptUserNote],
)
const updateDecryptionPassword = useCallback(
(password: string) => {
setDecryptionPassword(password)
tryDecryptUserNote()
},
[tryDecryptUserNote],
)
const {mutateAsync: refresh} = useMutation<
RefreshTokenResult,
AxiosError,
void
>(refreshToken, {
onError: () => logout(false),
})
const value = useMemo<AuthContextType>(
() => ({
user: user ?? null,
login: updateUser,
login: setUser,
logout,
isAuthenticated: user !== null,
_encryptContent: encryptContent,
_decryptContent: decryptContent,
_setDecryptionPassword: updateDecryptionPassword,
_updateUser: updateUser,
_encryptUsingMasterPassword: encryptUsingMasterPassword,
_decryptUsingMasterPassword: decryptUsingMasterPassword,
_decryptUsingPrivateKey: decryptUsingPrivateKey,
_setDecryptionPassword: setDecryptionPassword,
_updateUser: setUser,
}),
[
user,
logout,
encryptContent,
decryptContent,
updateDecryptionPassword,
updateUser,
],
[user, logout, encryptUsingMasterPassword, decryptUsingMasterPassword],
)
// Decrypt user notes
useEffect(() => {
if (
user &&
!user.isDecrypted &&
user.encryptedPassword &&
masterPassword
) {
const note = JSON.parse(
decryptUsingMasterPassword(user.encryptedNotes!),
)
const newUser: User = {
...user,
notes: note,
isDecrypted: true,
}
setUser(newUser)
}
}, [user, decryptUsingMasterPassword])
// Refresh token and logout user if needed
useEffect(() => {
const interceptor = client.interceptors.response.use(
response => response,

View File

@ -1,37 +1,28 @@
import {ReactElement} from "react"
import {ReactElement, useContext} from "react"
import {useAsync} from "react-use"
import {useUser} from "~/hooks"
import {decrypt, readMessage, readPrivateKey} from "openpgp"
import camelcaseKeys from "camelcase-keys"
import AuthContext from "~/AuthContext/AuthContext"
export interface DecryptedReportProps {
encryptedNotes: string
encryptedContent: string
}
export default function DecryptedReport({
encryptedNotes,
encryptedContent,
}: DecryptedReportProps): ReactElement {
const user = useUser()
const {_decryptUsingPrivateKey} = useContext(AuthContext)
const {value} = useAsync(async () => {
if (user.isDecrypted) {
// @ts-ignore
const key = await readPrivateKey({
armoredKey: user.notes.privateKey,
})
const message = await readMessage({
armoredMessage: encryptedNotes,
})
return await decrypt({
message: message,
decryptionKeys: key,
})
}
}, [encryptedNotes])
const message = await _decryptUsingPrivateKey(encryptedContent)
return camelcaseKeys(JSON.parse(message))
}, [encryptedContent])
if (!value) {
return <></>
}
return <div>{value}</div>
console.log(value)
return <></>
}

View File

@ -56,7 +56,7 @@ export default function AuthenticatedRoute(): ReactElement {
</Grid>
<Grid item xs={12} sm={8} md={10}>
<Paper>
<Box padding={4} maxHeight="60vh">
<Box padding={4} maxHeight="60vh" overflow="scroll">
<Outlet />
</Box>
</Paper>

View File

@ -25,7 +25,10 @@ export default function LoginRoute(): ReactElement {
/>,
<ConfirmCodeForm
key="confirm_code_form"
onConfirm={user => login(user, () => navigate("/"))}
onConfirm={user => {
login(user)
navigate("/")
}}
email={email}
sameRequestToken={sameRequestToken}
/>,

View File

@ -19,7 +19,7 @@ export default function ReportsRoute(): ReactElement {
{reports.map(report => (
<DecryptedReport
key={report.id}
encryptedNotes={report.encryptedNotes}
encryptedContent={report.encryptedContent}
/>
))}
</>

View File

@ -91,7 +91,7 @@ export interface Alias {
export interface Report {
id: string
encryptedNotes: string
encryptedContent: string
}
export interface UserNote {