-
Notifications
You must be signed in to change notification settings - Fork 19
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
feat: DAH-2876 useRedirect for authenticated pages #2488
Closed
Closed
Changes from 9 commits
Commits
Show all changes
62 commits
Select commit
Hold shift + click to select a range
99e823c
feat: DAH-2876 useRedirect for authenticated pages
cade-exygy b97e876
fix: translations
cade-exygy 8e6a74b
feat: use token for auth check
cade-exygy caba50e
feat: HOC withAuthentication
cade-exygy a086a37
clean: remove unused hook
cade-exygy 1137a56
fix: tests
cade-exygy 81eac08
fix: add back banner
cade-exygy bbb4c2d
fix: tests
cade-exygy a9c5089
fix: test
cade-exygy 1231935
fix: DAH-3097 FCFS link color contrast accessibility (#2482)
cliu02 f490077
feat: DAH-2982 directory page empty state (#2462)
tallulahkay 21318a1
fix: DAH-3118 update scroll behavior (#2477)
tallulahkay 2941219
feat: DAH-3041 remove field error on sign in page (#2471)
tallulahkay e26bf09
feat: DAH-3014 Listing detail event analytics (#2487)
chadbrokaw bbe88d7
fix: DAH-2738 hide listing details apply in mobile (#2490)
tallulahkay 3cb0467
fix: DAH-3014 Fix undefined lottery preferences bug (#2491)
chadbrokaw ae3e002
feat: DAH-3109 auto open on button navigation and highlight on moveme…
tallulahkay 8410de8
feat: DAH-3077 DALP hide lottery prefs section and skip validate hous…
tallulahkay 943241d
feat: DAH-2985 Autofill email for forgot password (#2485)
chadbrokaw f890d85
feat: DAH-3073 DALP priority data (#2493)
tallulahkay f7c0ffe
feat: DAH-3088 retrieve DALP loan officers (#2495)
tallulahkay 14803de
feat: DAH-3079 Change terms page for DALP listings (#2499)
chadbrokaw 6e90ce7
fix: DAH-3148 Send Listing Type in GTM (#2494)
chadbrokaw cc87461
feat: DAH-3122 Log Potential Google Translate API Usage (#2486)
jimlin-sfgov 192faf7
feat: DAH-3075 add dalp text to what to expect (#2496)
tallulahkay 5cf702d
feat: DAH-3103 remove work in sf question (#2497)
tallulahkay 1c3757d
feat: DAH-3157 use new document types for dalp (#2500)
tallulahkay 8b5dd5d
feat: DAH-2987 Reset password api (#2461)
chadbrokaw 66019fd
feat: DAH-3093 Update demographics description in application summary…
cliu02 f9ea085
fix: DAH-3122 Prevent translation rake tasks from triggering page_vie…
jimlin-sfgov ee76121
urgent: fix e2e fixture (#2507)
tallulahkay de24308
fix: DAH-3176 hide directory 1.5 change (#2508)
tallulahkay 68a149f
urgent: add checklist item for PA testing (#2506)
tallulahkay f41e7b3
fix: DAH-3111 Clear error message on Sign In (#2481)
cliu02 ae2151d
feat: DAH-3083 remove preferences in DALP application summary (#2492)
cliu02 3f4f122
fix: DAH-3181 Handle invalid listings when logging translation usage …
jimlin-sfgov 4d60e84
feat: DAH-3087 Remove back link on DALP listings (#2502)
chadbrokaw 76af9f8
fix: DAH-2865 translations fixes (#2473)
tallulahkay 413731c
fix: DAH-2867 make height auto on my accounts dashboard (#2475)
tallulahkay 5d4eb8c
fix: DAH-2868 status formatting (#2474)
tallulahkay 8019dbe
fix: DAH-3158 open sections upon navigation (#2504)
tallulahkay 7dcb7ae
feat: DAH-3110 make enter submit (#2479)
tallulahkay 822c2f1
fix: DAH-3193 change naming (#2518)
tallulahkay 048e88b
feat: DAH-3090 DAH-3076 DALP Qualify Checkbox Conditional Language (#…
tallulahkay fd3a3ca
fix: DAH-3190 Use translations on confirmation modals (#2519)
chadbrokaw 0c92c8d
feat: DAH-3055 Analytics for log in and log out (#2511)
chadbrokaw 5048264
feat: DAH-3080 DALP for My Applications page (#2509)
cliu02 db30bc8
fix: DAH-3187 update FCFS how to apply link translations (#2513)
cliu02 8c83653
feat: DAH-3091 Human Translations 2024-12-02 (#2514)
jimlin-sfgov 4bc7026
feat: DAH-3082 add dalp priority to conf page (#2522)
tallulahkay 4543971
fix: DAH-2866 fix formatting (#2476)
tallulahkay f3176b6
feat: DAH-3084 confirmation page DALP (#2521)
tallulahkay 8bdbd54
feat: DAH-3074 DALP Application Question (#2530)
jimlin-sfgov 43b0289
urgent: upgdate circle config (#2534)
tallulahkay e6dbd7b
feat: DAH-3169 default to sf gov listing (#2532)
tallulahkay 492e08d
feat: DAH-3132 Update icon (#2536)
tallulahkay e7b2173
feat: DAH-3085 DALP confirmation email (#2531)
tallulahkay bba991f
fix: DAH-3191 fix email address for lottery appeals (#2540)
tallulahkay 950eff2
feat: DAH-3201 Application Summary Edit button should take user to DA…
jimlin-sfgov 60dd7dc
feat: DAH-2826 Auto populate user (#2527)
chadbrokaw af1e40e
fix: DAH-3207 Use NPM Chromedriver for Protractor (#2544)
jimlin-sfgov 051e29f
fix: test conflicts
cade-exygy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
app/javascript/__tests__/authentication/withAuthentication.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import React from "react" | ||
import { render } from "@testing-library/react" | ||
import { withAuthentication } from "../../authentication/withAuthentication" | ||
import UserContext, { ContextProps } from "../../authentication/context/UserContext" | ||
import { isTokenValid } from "../../authentication/token" | ||
import { getLocalizedPath } from "../../util/routeUtil" | ||
import { getCurrentLanguage } from "../../util/languageUtil" | ||
|
||
jest.mock("../../authentication/token", () => ({ | ||
isTokenValid: jest.fn(), | ||
})) | ||
|
||
jest.mock("../../util/languageUtil", () => ({ | ||
getCurrentLanguage: jest.fn(() => "en"), | ||
getPathWithoutLanguagePrefix: jest.fn((path) => path), | ||
})) | ||
|
||
jest.mock("../../util/routeUtil", () => ({ | ||
getLocalizedPath: jest.fn((path) => path), | ||
})) | ||
|
||
describe("withAuthentication", () => { | ||
let originalLocation: Location | ||
let mockContextValue: ContextProps | ||
const TestComponent = () => <div>Protected Component</div> | ||
const WrappedComponent = withAuthentication(TestComponent) | ||
|
||
beforeEach(() => { | ||
originalLocation = window.location | ||
mockContextValue = { | ||
profile: { | ||
uid: "123", | ||
email: "[email protected]", | ||
created_at: new Date(), | ||
updated_at: new Date(), | ||
}, | ||
signIn: jest.fn(), | ||
signOut: jest.fn(), | ||
timeOut: jest.fn(), | ||
saveProfile: jest.fn(), | ||
loading: false, | ||
initialStateLoaded: true, | ||
} | ||
|
||
// Reset all mocks before each test | ||
jest.clearAllMocks() | ||
;(getCurrentLanguage as jest.Mock).mockReturnValue("en") | ||
|
||
// Mock window.location | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
delete (window as any).location | ||
window.location = { | ||
...originalLocation, | ||
href: "http://test.com", | ||
assign: jest.fn(), | ||
replace: jest.fn(), | ||
reload: jest.fn(), | ||
toString: jest.fn(), | ||
} | ||
}) | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
window.location = originalLocation | ||
}) | ||
|
||
it("renders the wrapped component when authenticated", () => { | ||
;(isTokenValid as jest.Mock).mockReturnValue(true) | ||
|
||
const { getByText } = render( | ||
<UserContext.Provider value={mockContextValue}> | ||
<WrappedComponent /> | ||
</UserContext.Provider> | ||
) | ||
|
||
expect(getByText("Protected Component")).toBeInTheDocument() | ||
}) | ||
|
||
it("redirects to sign-in when token is invalid", () => { | ||
;(isTokenValid as jest.Mock).mockReturnValue(false) | ||
;(getLocalizedPath as jest.Mock).mockReturnValue("/sign-in") | ||
|
||
render( | ||
<UserContext.Provider value={mockContextValue}> | ||
<WrappedComponent /> | ||
</UserContext.Provider> | ||
) | ||
|
||
expect(getLocalizedPath).toHaveBeenCalledWith("/sign-in", "en", "") | ||
expect(window.location.href).toBe("/sign-in") | ||
}) | ||
|
||
it("redirects to sign-in with redirect param when specified", () => { | ||
;(isTokenValid as jest.Mock).mockReturnValue(false) | ||
;(getLocalizedPath as jest.Mock).mockReturnValue("/sign-in?redirect=test-path") | ||
const WrappedWithRedirect = withAuthentication(TestComponent, { redirectPath: "test-path" }) | ||
|
||
render( | ||
<UserContext.Provider value={mockContextValue}> | ||
<WrappedWithRedirect /> | ||
</UserContext.Provider> | ||
) | ||
|
||
expect(getLocalizedPath).toHaveBeenCalledWith("/sign-in", "en", "?redirect=test-path") | ||
expect(window.location.href).toBe("/sign-in?redirect=test-path") | ||
}) | ||
|
||
it("returns null while loading", () => { | ||
;(isTokenValid as jest.Mock).mockReturnValue(true) | ||
mockContextValue.loading = true | ||
|
||
const { container } = render( | ||
<UserContext.Provider value={mockContextValue}> | ||
<WrappedComponent /> | ||
</UserContext.Provider> | ||
) | ||
|
||
expect(container.firstChild).toBeNull() | ||
}) | ||
|
||
it("returns null when profile is undefined", () => { | ||
;(isTokenValid as jest.Mock).mockReturnValue(true) | ||
mockContextValue.profile = undefined | ||
|
||
const { container } = render( | ||
<UserContext.Provider value={mockContextValue}> | ||
<WrappedComponent /> | ||
</UserContext.Provider> | ||
) | ||
|
||
expect(container.firstChild).toBeNull() | ||
}) | ||
|
||
it("sets display name for debugging", () => { | ||
const TestComponentWithName = () => <div>Named Component</div> | ||
TestComponentWithName.displayName = "TestComponentWithName" | ||
const WrappedWithName = withAuthentication(TestComponentWithName) | ||
|
||
expect(WrappedWithName.displayName).toBe("WithAuthentication(TestComponentWithName)") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from "react" | ||
import { isTokenValid } from "./token" | ||
import UserContext from "./context/UserContext" | ||
import { getLocalizedPath } from "../util/routeUtil" | ||
import { getCurrentLanguage } from "../util/languageUtil" | ||
|
||
interface WithAuthenticationProps { | ||
redirectPath?: string | ||
} | ||
|
||
/** | ||
* Higher-order component that handles authentication for protected routes. | ||
* It will: | ||
* 1. Check for a valid token on mount | ||
* 2. Show nothing while profile is loading | ||
* 3. Redirect to sign-in if no valid token or no profile | ||
* 4. Render the wrapped component only when authenticated | ||
* | ||
* @param WrappedComponent The component to protect | ||
* @param redirectPath Optional path to redirect back to after sign-in | ||
*/ | ||
export const withAuthentication = <P extends object>( | ||
WrappedComponent: React.ComponentType<P>, | ||
{ redirectPath }: WithAuthenticationProps = {} | ||
) => { | ||
const WithAuthenticationComponent = (props: P) => { | ||
const { profile, loading } = React.useContext(UserContext) | ||
|
||
React.useEffect(() => { | ||
if (!isTokenValid()) { | ||
const redirectParam = redirectPath ? `?redirect=${redirectPath}` : "" | ||
const language = getCurrentLanguage() | ||
const signInPath = getLocalizedPath("/sign-in", language, redirectParam) | ||
window.location.href = signInPath | ||
} | ||
}, []) | ||
|
||
if (loading) { | ||
return null | ||
} | ||
|
||
if (!profile) { | ||
return null | ||
} | ||
|
||
return <WrappedComponent {...props} /> | ||
} | ||
|
||
// Set display name for easier debugging | ||
WithAuthenticationComponent.displayName = `WithAuthentication(${ | ||
WrappedComponent.displayName || WrappedComponent.name || "Component" | ||
})` | ||
|
||
return WithAuthenticationComponent | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
thought: Storing redirect information in the URL is a departure from the previous iterations of this PR, and from the Angular version of the website. I'd love to get a breakdown of this decision. CC @chadbrokaw since I believe he is also quite involved with the authentication work.
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.
Yeah for sure! In short, handling the redirect with url params allows us to avoid using session storage to keep track of the redirect state across page reloads. I feel like this is a more "React like" way of handling this, since the SignIn component now fully controls when to show the banner and adjust the redirect after submit, instead of relying on a session storage value being set (and then cleared appropriately). Since the banner is toggled only on the presence of the redirect flag, it is more clear why and when that happens imo. The withAuthentication HOC also provides us a way to handle adding this param for any page that we want to be gated behind an authentication state.