-
Notifications
You must be signed in to change notification settings - Fork 2
BA-1809 [FE] Simplify CurrentProfileProvider #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4b85c85
ea1bddd
e28a03f
7adab05
6bb3e39
907e794
ded5f41
29fad6e
53b3829
33b074f
9175c12
13ad9b7
650c542
341f377
7e840df
5892726
d56df60
89044ac
e69248c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { useState } from 'react' | |||||||||||||
| import { | ||||||||||||||
| ACCESS_KEY_NAME, | ||||||||||||||
| REFRESH_KEY_NAME, | ||||||||||||||
| decodeJWT, | ||||||||||||||
| setFormApiErrors, | ||||||||||||||
| setTokenAsync, | ||||||||||||||
| } from '@baseapp-frontend/utils' | ||||||||||||||
|
|
@@ -21,11 +22,13 @@ import type { | |||||||||||||
| LoginMfaRequest, | ||||||||||||||
| LoginRequest, | ||||||||||||||
| } from '../../../types/auth' | ||||||||||||||
| import { User } from '../../../types/user' | ||||||||||||||
| import { | ||||||||||||||
| isLoginChangeExpiredPasswordRedirectResponse, | ||||||||||||||
| isLoginMfaResponse, | ||||||||||||||
| } from '../../../utils/login' | ||||||||||||||
| import { CODE_VALIDATION_INITIAL_VALUES, CODE_VALIDATION_SCHEMA } from '../../mfa/constants' | ||||||||||||||
| import { useCurrentProfile } from '../../profile' | ||||||||||||||
| import { DEFAULT_INITIAL_VALUES, DEFAULT_VALIDATION_SCHEMA } from './constants' | ||||||||||||||
| import type { UseLoginOptions } from './types' | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -39,6 +42,7 @@ const useLogin = ({ | |||||||||||||
| enableFormApiErrors = true, | ||||||||||||||
| }: UseLoginOptions = {}) => { | ||||||||||||||
| const [mfaEphemeralToken, setMfaEphemeralToken] = useState<string | null>(null) | ||||||||||||||
| const { setCurrentProfile } = useCurrentProfile() | ||||||||||||||
|
|
||||||||||||||
| /* | ||||||||||||||
| * Handles login success with the auth token in response | ||||||||||||||
|
|
@@ -49,6 +53,16 @@ const useLogin = ({ | |||||||||||||
| if (isLoginChangeExpiredPasswordRedirectResponse(response)) { | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| const user = decodeJWT<User>(response.access) | ||||||||||||||
| if (user) { | ||||||||||||||
| // TODO: handle the absolute image path on the backend | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Based on the evidence gathered, I can now provide the final response: Image path handling needs to be moved to the backend as planned The TODO comment is valid as the current implementation in multiple files shows client-side image path construction:
Both files use 🔗 Analysis chainBackend should handle image path construction. The TODO comment indicates that image path handling should be moved to the backend. This is still being handled on the client side, which is not aligned with the security best practices. Let's check if this is being addressed in other parts of the codebase: Consider creating a dedicated API endpoint for profile data that returns fully qualified image URLs. Let me verify if there are any backend implementations or ongoing work related to this image path handling. Let me check one more thing to see if there are any backend API implementations or configurations related to image handling. 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Search for similar image path handling patterns
rg -A 3 "NEXT_PUBLIC_API_BASE_URL.*image"
# Check for any backend-related changes
rg -A 3 "TODO.*handle.*backend"
Length of output: 1098 Script: #!/bin/bash
# Check backend-related files for image URL handling
ast-grep --pattern 'function $FUNC($$$) {
$$$
url: $$$
$$$
}'
# Look for API endpoints related to profile or image handling
rg -l "router.(get|post).*profile"
# Check if there are any recent changes or PRs addressing this
rg -l "image.*url"
Length of output: 1433 Script: #!/bin/bash
# Look for backend API configurations or implementations
rg -A 5 "createUrl|getImageUrl|imageUrl"
# Check for any image-related configurations in the backend
fd -t f "config|settings" -x rg -l "image.*path|image.*url" {}
Length of output: 6251 |
||||||||||||||
| const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace('/v1', '') | ||||||||||||||
| const absoluteImagePath = user.profile.image ? `${baseUrl}${user.profile.image}` : null | ||||||||||||||
|
||||||||||||||
| const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace('/v1', '') | |
| const absoluteImagePath = user.profile.image ? `${baseUrl}${user.profile.image}` : null | |
| const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace('/v1', '') | |
| const absoluteImagePath = user.profile.image | |
| ? `${baseUrl}${user.profile.image.replace(/\.{2,}/g, '')}` | |
| : null |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { default as useCurrentProfile } from './useCurrentProfile' | ||
| export type { MinimalProfile } from '../../types/profile' | ||
|
|
||
| export { InitialProfileProvider } from './useCurrentProfile/__tests__/__utils__/InitialProfileProvider' | ||
| export type { InitialProfileProp } from './useCurrentProfile/__tests__/__utils__/InitialProfileProvider' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { faker } from '@faker-js/faker' | ||
|
|
||
| export const mockUserProfileFactory = (id: string) => { | ||
| return { | ||
| id, | ||
| name: faker.person.fullName(), | ||
| image: faker.image.avatar(), | ||
| urlPath: faker.internet.url(), | ||
| } | ||
| } | ||
tsl-ps2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { FC, PropsWithChildren } from 'react' | ||
|
|
||
| import { useHydrateAtoms } from 'jotai/utils' | ||
|
|
||
| import { MinimalProfile } from '../../../../../../types/profile' | ||
| import { profileAtom } from '../../../index' | ||
|
|
||
| export type InitialProfileProp = { | ||
| initialProfile: MinimalProfile | null | ||
| } | ||
|
|
||
| // Use as: | ||
| // <JotaiProvider> | ||
| // <InitialProfileProvider initialProfile={props.initialProfile}> | ||
| // // You're component goes here, it is passed the initialProfile | ||
| // </InitialProfileProvider> | ||
| // </JotaiProvider> | ||
|
|
||
| export const InitialProfileProvider: FC<PropsWithChildren & InitialProfileProp> = ({ | ||
| initialProfile, | ||
| children, | ||
| }) => { | ||
| useHydrateAtoms([[profileAtom, initialProfile]]) | ||
| return children | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { act, renderHook } from '@baseapp-frontend/test' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LOGOUT_EVENT, eventEmitter, removeCookie, setCookie } from '@baseapp-frontend/utils' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import useCurrentProfile from '..' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CURRENT_PROFILE_KEY } from '../constants' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { mockUserProfileFactory } from './__mock__/profiles' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jest.mock('@baseapp-frontend/utils', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...jest.requireActual('@baseapp-frontend/utils'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removeCookie: jest.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setCookie: jest.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('useCurrentProfile', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jest.clearAllMocks() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('changes current profile state and sets cookie', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const profile1 = mockUserProfileFactory('profile-id-1') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const profile2 = mockUserProfileFactory('profile-id-2') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { result } = renderHook(() => useCurrentProfile()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| act(() => result.current.setCurrentProfile(profile1)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.current.currentProfile!.id).toEqual('profile-id-1') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile1, { stringfyValue: true }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| act(() => result.current.setCurrentProfile(profile2)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.current.currentProfile!.id).toEqual('profile-id-2') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile1, { stringfyValue: true }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('changes current profile state and sets cookie', () => { | |
| const profile1 = mockUserProfileFactory('profile-id-1') | |
| const profile2 = mockUserProfileFactory('profile-id-2') | |
| const { result } = renderHook(() => useCurrentProfile()) | |
| act(() => result.current.setCurrentProfile(profile1)) | |
| expect(result.current.currentProfile!.id).toEqual('profile-id-1') | |
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile1, { stringfyValue: true }) | |
| act(() => result.current.setCurrentProfile(profile2)) | |
| expect(result.current.currentProfile!.id).toEqual('profile-id-2') | |
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile1, { stringfyValue: true }) | |
| }) | |
| it('changes current profile state and sets cookie', () => { | |
| const profile1 = mockUserProfileFactory('profile-id-1') | |
| const profile2 = mockUserProfileFactory('profile-id-2') | |
| const { result } = renderHook(() => useCurrentProfile()) | |
| act(() => result.current.setCurrentProfile(profile1)) | |
| expect(result.current.currentProfile!.id).toEqual('profile-id-1') | |
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile1, { stringfyValue: true }) | |
| act(() => result.current.setCurrentProfile(profile2)) | |
| expect(result.current.currentProfile!.id).toEqual('profile-id-2') | |
| expect(setCookie).toHaveBeenCalledWith(CURRENT_PROFILE_KEY, profile2, { stringfyValue: true }) | |
| }) |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test coverage for critical scenarios
The current test suite is missing coverage for several key scenarios introduced in this PR:
- Initial profile loading from JWT claims:
it('should initialize profile from JWT claims', () => {
const mockClaims = { profileId: 'jwt-profile-id' }
parseJwt.mockReturnValue(mockClaims)
const { result } = renderHook(() => useCurrentProfile())
expect(result.current.currentProfile).toEqual(expect.objectContaining({ id: 'jwt-profile-id' }))
})- SSR behavior with cookie:
it('should handle SSR profile initialization', () => {
isSSR.mockReturnValue(true)
const profile = mockUserProfileFactory('ssr-profile')
// Mock cookie retrieval for SSR
const { result } = renderHook(() => useCurrentProfile({ noSSR: false }))
expect(result.current.currentProfile).toEqual(profile)
})- Profile updates with active check:
it('should only update if profile is active', () => {
const profile = mockUserProfileFactory('active-profile')
const { result } = renderHook(() => useCurrentProfile())
act(() => result.current.updateProfileIfActive(profile))
expect(result.current.currentProfile).toEqual(profile)
})| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const CURRENT_PROFILE_KEY = 'CurrentProfile' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| 'use client' | ||
|
|
||
| import { useCallback, useEffect } from 'react' | ||
|
|
||
| import { | ||
| LOGOUT_EVENT, | ||
| ServerSideRenderingOption, | ||
| eventEmitter, | ||
| getCookie, | ||
| removeCookie, | ||
| setCookie, | ||
| } from '@baseapp-frontend/utils' | ||
|
|
||
| import { atom, useAtom } from 'jotai' | ||
|
|
||
| import { MinimalProfile } from '../../../types/profile' | ||
| import { CURRENT_PROFILE_KEY } from './constants' | ||
|
|
||
| export const getProfileFromCookie = ({ noSSR = true }: ServerSideRenderingOption = {}) => { | ||
| const settings = | ||
| getCookie<MinimalProfile | undefined>(CURRENT_PROFILE_KEY, { noSSR, parseJSON: true }) ?? null | ||
|
|
||
| return settings | ||
| } | ||
tsl-ps2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const initialProfile = getProfileFromCookie() | ||
|
|
||
| export const profileAtom = atom<MinimalProfile | null>(initialProfile) | ||
|
|
||
| /** | ||
| * By using `useCurrentProfile` with the `noSSR` option set to `false`, causes Next.js to dynamically render the affected pages, instead of statically rendering them. | ||
| */ | ||
| const useCurrentProfile = ({ noSSR = true }: ServerSideRenderingOption = {}) => { | ||
| const [currentProfile, setProfile] = useAtom(profileAtom) | ||
| const isSSR = typeof window === typeof undefined | ||
tsl-ps2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const setCurrentProfile = (newProfile: MinimalProfile | null) => { | ||
| if (newProfile === null) { | ||
| setProfile(() => { | ||
| removeCookie(CURRENT_PROFILE_KEY) | ||
| return null | ||
| }) | ||
| } else { | ||
| setProfile(() => { | ||
| setCookie(CURRENT_PROFILE_KEY, newProfile, { stringfyValue: true }) | ||
| return newProfile | ||
| }) | ||
| } | ||
| } | ||
tsl-ps2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const updateProfileIfActive = (newProfile: MinimalProfile) => { | ||
| if (currentProfile?.id === newProfile.id) { | ||
| setCurrentProfile(newProfile) | ||
| } | ||
| } | ||
|
|
||
| const removeCurrentProfile = useCallback(() => setCurrentProfile(null), []) | ||
|
||
|
|
||
| useEffect(() => { | ||
| eventEmitter.on(LOGOUT_EVENT, removeCurrentProfile) | ||
| return () => { | ||
| eventEmitter.off(LOGOUT_EVENT, removeCurrentProfile) | ||
| } | ||
| }, []) | ||
|
|
||
| if (isSSR) { | ||
| return { | ||
| currentProfile: getProfileFromCookie({ noSSR }), | ||
| setCurrentProfile, | ||
| updateProfileIfActive, | ||
| } | ||
| } | ||
| return { | ||
| currentProfile, | ||
| setCurrentProfile, | ||
| updateProfileIfActive, | ||
| } | ||
| } | ||
|
|
||
| export default useCurrentProfile | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' | |
|
|
||
| import UserApi, { USER_API_KEY } from '../../../services/user' | ||
| import type { User, UserUpdateParams } from '../../../types/user' | ||
| import { useCurrentProfile } from '../../profile' | ||
| import type { UseUpdateUserOptions } from './types' | ||
|
|
||
| const useUpdateUser = <TUser extends Pick<User, 'id'>>({ | ||
|
|
@@ -13,6 +14,7 @@ const useUpdateUser = <TUser extends Pick<User, 'id'>>({ | |
| ApiClass = UserApi, | ||
| }: UseUpdateUserOptions<TUser> = {}) => { | ||
| const queryClient = useQueryClient() | ||
| const { setCurrentProfile } = useCurrentProfile() | ||
|
|
||
| const mutation = useMutation({ | ||
| mutationFn: (params: UserUpdateParams<TUser>) => ApiClass.updateUser<TUser>(params), | ||
|
|
@@ -23,6 +25,7 @@ const useUpdateUser = <TUser extends Pick<User, 'id'>>({ | |
| } catch (e) { | ||
| // silently fail | ||
| // eslint-disable-next-line no-console | ||
| setCurrentProfile(null) | ||
|
||
| console.error(e) | ||
| } | ||
| options?.onSettled?.(data, error, variables, context) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export type MinimalProfile = { | ||
| id: string | ||
| name: string | null | ||
| image: string | null | ||
| urlPath: string | null | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,22 @@ | ||
| import { MinimalProfile } from './profile' | ||
|
|
||
| export interface User { | ||
| id: number | ||
| email: string | ||
| isEmailVerified: boolean | ||
| newEmail: string | ||
| isNewEmailConfirmed: boolean | ||
| referralCode: string | ||
| avatar: { | ||
| fullSize: string | ||
| small: string | ||
| } | ||
| firstName: string | ||
| lastName: string | ||
| profile: MinimalProfile | ||
| phoneNumber: string | ||
| preferredLanguage: string | ||
| } | ||
|
|
||
| export interface UserUpdateParams<TUser extends Partial<User>> { | ||
| userId: TUser['id'] | ||
| data: Partial<Omit<TUser, 'avatar' | 'id'>> & { | ||
| data: Partial<Omit<TUser, 'id'>> & { | ||
| avatar?: File | string | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,3 @@ | ||
| module.exports = { | ||
| Public_Sans: () => ({ | ||
| className: 'mock-public-sans', | ||
| style: {}, | ||
| }), | ||
| Barlow: () => ({ | ||
| className: 'mock-barlow', | ||
| style: {}, | ||
| }), | ||
| } | ||
| module.exports = require('@baseapp-frontend/test/__mocks__/nextFontMock.ts') | ||
|
|
||
| export {} |
Uh oh!
There was an error while loading. Please reload this page.