mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
fixed password setup not working; general improvements & bugfixes
This commit is contained in:
parent
4ab7303071
commit
934a071f81
@ -11,13 +11,13 @@ export enum EncryptionStatus {
|
||||
interface AuthContextTypeBase {
|
||||
user: ServerUser | User | null
|
||||
isAuthenticated: boolean
|
||||
login: (user: ServerUser | User) => void
|
||||
login: (user: ServerUser | User, callback?: () => void) => void
|
||||
logout: () => void
|
||||
encryptionStatus: EncryptionStatus
|
||||
_decryptUsingMasterPassword: (content: string) => string
|
||||
_encryptUsingMasterPassword: (content: string) => string
|
||||
_decryptUsingPrivateKey: (message: string) => Promise<string>
|
||||
_setDecryptionPassword: (decryptionPassword: string) => boolean
|
||||
_setDecryptionPassword: (decryptionPassword: string, callback?: () => void) => boolean
|
||||
_updateUser: (user: ServerUser | User) => void
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ReactElement, ReactNode, useCallback} from "react"
|
||||
import {ReactElement, ReactNode, useCallback} from "react"
|
||||
import {useLocalStorage} from "react-use"
|
||||
import fastHashCode from "fast-hash-code";
|
||||
import fastHashCode from "fast-hash-code"
|
||||
|
||||
import {ServerUser, User} from "~/server-types"
|
||||
|
||||
@ -9,7 +9,7 @@ import PasswordShareConfirmationDialog from "./PasswordShareConfirmationDialog"
|
||||
import useContextValue from "./use-context-value"
|
||||
import useExtensionHandler from "./use-extension-handler"
|
||||
import useMasterPassword from "./use-master-password"
|
||||
import useUser from "./use-user";
|
||||
import useUser from "./use-user"
|
||||
|
||||
export interface AuthContextProviderProps {
|
||||
children: ReactNode
|
||||
@ -26,9 +26,11 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
|
||||
decryptUsingMasterPassword,
|
||||
decryptUsingPrivateKey,
|
||||
setDecryptionPassword,
|
||||
decryptionPasswordHash,
|
||||
_masterPassword,
|
||||
logout: logoutMasterPassword,
|
||||
} = useMasterPassword(user || null)
|
||||
const passwordHash = _masterPassword ? fastHashCode(_masterPassword).toString() : null
|
||||
const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler(
|
||||
_masterPassword!,
|
||||
user as User,
|
||||
@ -39,10 +41,11 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
|
||||
}, [logoutMasterPassword])
|
||||
|
||||
const contextValue = useContextValue({
|
||||
decryptUsingPrivateKey,
|
||||
encryptUsingMasterPassword,
|
||||
decryptUsingMasterPassword,
|
||||
setDecryptionPassword,
|
||||
_decryptUsingPrivateKey: decryptUsingPrivateKey,
|
||||
_encryptUsingMasterPassword: encryptUsingMasterPassword,
|
||||
_decryptUsingMasterPassword: decryptUsingMasterPassword,
|
||||
_setDecryptionPassword: setDecryptionPassword,
|
||||
decryptionPasswordHash,
|
||||
logout,
|
||||
login: setUser,
|
||||
user: user || null,
|
||||
@ -53,7 +56,7 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
|
||||
decryptUsingMasterPassword,
|
||||
user: user || null,
|
||||
updateUser: setUser,
|
||||
masterPasswordHash: _masterPassword ? fastHashCode(_masterPassword).toString() : null,
|
||||
masterPasswordHash: passwordHash,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -1,49 +1,86 @@
|
||||
import {useMemo, useRef} from "react"
|
||||
import {useUpdateEffect} from "react-use"
|
||||
|
||||
import {AuthContextType, EncryptionStatus} from "~/AuthContext/AuthContext"
|
||||
import {ServerUser, User} from "~/server-types"
|
||||
|
||||
export interface UseContextValueData {
|
||||
user: User | ServerUser | null
|
||||
export type UseContextValueData = Pick<
|
||||
AuthContextType,
|
||||
| "user"
|
||||
| "logout"
|
||||
| "_encryptUsingMasterPassword"
|
||||
| "_decryptUsingMasterPassword"
|
||||
| "_decryptUsingPrivateKey"
|
||||
> & {
|
||||
decryptionPasswordHash: string
|
||||
_setDecryptionPassword: (password: string) => boolean
|
||||
login: (user: User | ServerUser) => void
|
||||
logout: () => void
|
||||
encryptUsingMasterPassword: (content: string) => string
|
||||
decryptUsingMasterPassword: (content: string) => string
|
||||
decryptUsingPrivateKey: (message: string) => Promise<string>
|
||||
setDecryptionPassword: (password: string) => boolean
|
||||
}
|
||||
|
||||
export default function useContextValue({
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
encryptUsingMasterPassword,
|
||||
decryptUsingMasterPassword,
|
||||
setDecryptionPassword,
|
||||
decryptUsingPrivateKey,
|
||||
_encryptUsingMasterPassword,
|
||||
_decryptUsingMasterPassword,
|
||||
_setDecryptionPassword,
|
||||
_decryptUsingPrivateKey,
|
||||
decryptionPasswordHash,
|
||||
}: UseContextValueData): AuthContextType {
|
||||
return {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: Boolean(user),
|
||||
encryptionStatus: (() => {
|
||||
if (!user) {
|
||||
return EncryptionStatus.Unavailable
|
||||
}
|
||||
const $decryptionPasswordChangeCallback = useRef<(() => void) | null>(null)
|
||||
const $userChangeCallback = useRef<(() => void) | null>(null)
|
||||
|
||||
if (!user.encryptedPassword) {
|
||||
return EncryptionStatus.Unavailable
|
||||
}
|
||||
useUpdateEffect(() => {
|
||||
$decryptionPasswordChangeCallback.current?.()
|
||||
}, [decryptionPasswordHash, user])
|
||||
|
||||
if (user.isDecrypted) {
|
||||
return EncryptionStatus.Available
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
user,
|
||||
login: (user, callback) => {
|
||||
if (callback) {
|
||||
$userChangeCallback.current = callback
|
||||
}
|
||||
|
||||
return EncryptionStatus.PasswordRequired
|
||||
})(),
|
||||
_updateUser: login,
|
||||
_setDecryptionPassword: setDecryptionPassword,
|
||||
_encryptUsingMasterPassword: encryptUsingMasterPassword,
|
||||
_decryptUsingMasterPassword: decryptUsingMasterPassword,
|
||||
_decryptUsingPrivateKey: decryptUsingPrivateKey,
|
||||
}
|
||||
return login(user)
|
||||
},
|
||||
logout,
|
||||
isAuthenticated: Boolean(user),
|
||||
encryptionStatus: (() => {
|
||||
if (!user) {
|
||||
return EncryptionStatus.Unavailable
|
||||
}
|
||||
|
||||
if (!user.encryptedPassword) {
|
||||
return EncryptionStatus.Unavailable
|
||||
}
|
||||
|
||||
if (user.isDecrypted) {
|
||||
return EncryptionStatus.Available
|
||||
}
|
||||
|
||||
return EncryptionStatus.PasswordRequired
|
||||
})(),
|
||||
_updateUser: login,
|
||||
_setDecryptionPassword: (password, callback) => {
|
||||
if (callback) {
|
||||
$decryptionPasswordChangeCallback.current = callback
|
||||
}
|
||||
|
||||
return _setDecryptionPassword(password)
|
||||
},
|
||||
_encryptUsingMasterPassword,
|
||||
_decryptUsingMasterPassword,
|
||||
_decryptUsingPrivateKey,
|
||||
}),
|
||||
[
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
_setDecryptionPassword,
|
||||
_encryptUsingMasterPassword,
|
||||
_decryptUsingMasterPassword,
|
||||
_decryptUsingPrivateKey,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import {useLocalStorage} from "react-use"
|
||||
import {useCallback, useMemo} from "react"
|
||||
import {decrypt, readMessage, readPrivateKey} from "openpgp";
|
||||
import {decrypt, readMessage, readPrivateKey} from "openpgp"
|
||||
import fastHashCode from "fast-hash-code"
|
||||
|
||||
import {decryptString, encryptString} from "~/utils"
|
||||
import {ServerUser, User} from "~/server-types";
|
||||
import {ServerUser, User} from "~/server-types"
|
||||
|
||||
export interface UseMasterPasswordResult {
|
||||
encryptUsingMasterPassword: (content: string) => string
|
||||
@ -12,13 +13,12 @@ export interface UseMasterPasswordResult {
|
||||
|
||||
setDecryptionPassword: (password: string) => boolean
|
||||
logout: () => void
|
||||
decryptionPasswordHash: string
|
||||
// Use this cautiously
|
||||
_masterPassword: string
|
||||
}
|
||||
|
||||
export default function useMasterPassword(
|
||||
user: User | ServerUser | null,
|
||||
): UseMasterPasswordResult {
|
||||
export default function useMasterPassword(user: User | ServerUser | null): UseMasterPasswordResult {
|
||||
const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>(
|
||||
"_global-context-auth-decryption-password",
|
||||
null,
|
||||
@ -78,21 +78,23 @@ export default function useMasterPassword(
|
||||
[user],
|
||||
)
|
||||
|
||||
const updateDecryptionPassword = useCallback((password: string) => {
|
||||
if (!user || !user.encryptedPassword) {
|
||||
throw new Error("User not set.")
|
||||
}
|
||||
const updateDecryptionPassword = useCallback(
|
||||
(password: string) => {
|
||||
if (user?.encryptedPassword) {
|
||||
try {
|
||||
const masterPassword = decryptString(user.encryptedPassword, password)
|
||||
JSON.parse(decryptString((user as ServerUser).encryptedNotes, masterPassword))
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const masterPassword = decryptString(user.encryptedPassword, password)
|
||||
JSON.parse(decryptString((user as ServerUser).encryptedNotes, masterPassword))
|
||||
setDecryptionPassword(password)
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [user, masterPassword])
|
||||
return true
|
||||
},
|
||||
[user, masterPassword],
|
||||
)
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setDecryptionPassword(null)
|
||||
@ -105,5 +107,6 @@ export default function useMasterPassword(
|
||||
logout,
|
||||
setDecryptionPassword: updateDecryptionPassword,
|
||||
_masterPassword: masterPassword!,
|
||||
decryptionPasswordHash: fastHashCode(decryptionPassword || "").toString(),
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +1,74 @@
|
||||
import {Dispatch, SetStateAction, useEffect} from "react";
|
||||
import {AxiosError} from "axios";
|
||||
import {Dispatch, SetStateAction, useEffect} from "react"
|
||||
import {AxiosError} from "axios"
|
||||
|
||||
import {useMutation, useQuery} from "@tanstack/react-query";
|
||||
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";
|
||||
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>>
|
||||
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,
|
||||
logout,
|
||||
masterPasswordHash,
|
||||
decryptUsingMasterPassword,
|
||||
user,
|
||||
updateUser,
|
||||
}: UseAuthData) {
|
||||
const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, {
|
||||
onError: () => logout(),
|
||||
})
|
||||
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,
|
||||
})
|
||||
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!))
|
||||
// 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])
|
||||
updateUser({
|
||||
...user,
|
||||
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
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request.responseURL === REFRESH_TOKEN_URL) {
|
||||
await logout()
|
||||
} else {
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
},
|
||||
)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
||||
return () => client.interceptors.response.eject(interceptor)
|
||||
}, [logout, refresh])
|
||||
return () => client.interceptors.response.eject(interceptor)
|
||||
}, [logout, refresh])
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import passwordGenerator from "secure-random-password"
|
||||
import {PasswordField, SimpleForm} from "~/components"
|
||||
import {buildEncryptionPassword, encryptString} from "~/utils"
|
||||
import {isDev} from "~/constants/development"
|
||||
import {useExtensionHandler, useSystemPreferredTheme, useUser} from "~/hooks"
|
||||
import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks"
|
||||
import {MASTER_PASSWORD_LENGTH} from "~/constants/values"
|
||||
import {AuthenticationDetails, UserNote} from "~/server-types"
|
||||
import {UpdateAccountData, updateAccount} from "~/apis"
|
||||
@ -34,6 +34,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
|
||||
const user = useUser()
|
||||
const theme = useSystemPreferredTheme()
|
||||
|
||||
const navigateToNext = useNavigateToNext()
|
||||
const $password = useRef<HTMLInputElement | null>(null)
|
||||
const $passwordConfirmation = useRef<HTMLInputElement | null>(null)
|
||||
const schema = yup.object().shape({
|
||||
@ -65,12 +66,6 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
|
||||
)
|
||||
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, UpdateAccountData>(
|
||||
updateAccount,
|
||||
{
|
||||
onSuccess: ({user}) => {
|
||||
login(user)
|
||||
setTimeout(onDone, 0)
|
||||
},
|
||||
},
|
||||
)
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: schema,
|
||||
@ -97,19 +92,25 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
|
||||
}
|
||||
const encryptedNotes = encryptUserNote(note, masterPassword)
|
||||
|
||||
_setDecryptionPassword(encryptionPassword)
|
||||
|
||||
await mutateAsync({
|
||||
encryptedPassword: encryptedMasterPassword,
|
||||
publicKey: (
|
||||
await readKey({
|
||||
armoredKey: keyPair.publicKey,
|
||||
})
|
||||
)
|
||||
.toPublic()
|
||||
.armor(),
|
||||
encryptedNotes,
|
||||
})
|
||||
await mutateAsync(
|
||||
{
|
||||
encryptedPassword: encryptedMasterPassword,
|
||||
publicKey: (
|
||||
await readKey({
|
||||
armoredKey: keyPair.publicKey,
|
||||
})
|
||||
)
|
||||
.toPublic()
|
||||
.armor(),
|
||||
encryptedNotes,
|
||||
},
|
||||
{
|
||||
onSuccess: ({user: newUser}) => {
|
||||
login(newUser)
|
||||
_setDecryptionPassword(encryptionPassword, navigateToNext)
|
||||
},
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
setErrors({detail: t("general.defaultError")})
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export default function ConfirmCodeForm({
|
||||
sameRequestToken,
|
||||
}: ConfirmCodeFormProps): ReactElement {
|
||||
const settings = useLoaderData() as ServerSettings
|
||||
const expirationTime = isDev ? 9 : settings.emailLoginExpirationInSeconds
|
||||
const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
|
||||
const {t} = useTranslation()
|
||||
const requestDate = useMemo(() => new Date(), [])
|
||||
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
|
||||
|
@ -36,15 +36,14 @@ export default function EnterDecryptionPassword(): ReactElement {
|
||||
onSubmit: async ({password}, {setErrors}) => {
|
||||
const decryptionPassword = buildEncryptionPassword(password, user.email.address)
|
||||
|
||||
console.log("decryptionPassword", decryptionPassword)
|
||||
if (!_setDecryptionPassword(decryptionPassword)) {
|
||||
const isPasswordCorrect = _setDecryptionPassword(decryptionPassword, navigateToNext)
|
||||
|
||||
if (!isPasswordCorrect) {
|
||||
setErrors({
|
||||
password: t(
|
||||
"components.EnterDecryptionPassword.form.password.errors.invalidPassword",
|
||||
),
|
||||
})
|
||||
} else {
|
||||
setTimeout(navigateToNext, 0)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user