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", "@tanstack/react-query": "^4.12.0",
"axios": "^1.1.2", "axios": "^1.1.2",
"axios-case-converter": "^0.11.1", "axios-case-converter": "^0.11.1",
"camelcase-keys": "^8.0.2",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"formik": "^2.2.9", "formik": "^2.2.9",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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