-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create generic credential and profile reducers (#178)
- Loading branch information
Fran McDade
authored and
Fran McDade
committed
Oct 9, 2024
1 parent
539e30d
commit 82b5f74
Showing
25 changed files
with
757 additions
and
0 deletions.
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
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,16 @@ | ||
export type Credentials = string; | ||
|
||
export interface AuthenticationService { | ||
login: () => void; | ||
logout: () => void; | ||
} | ||
|
||
export interface OAuthResponse { | ||
access_token: string; | ||
} | ||
|
||
export interface OAuthRevokeResponse { | ||
error?: boolean; | ||
error_description?: string; | ||
successful?: boolean; | ||
} |
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,105 @@ | ||
import { Dispatch } from "react"; | ||
import { AuthProviderConfig } from "../../../../config/entities"; | ||
import { | ||
requestCredentials, | ||
revokeCredentials, | ||
} from "../../../../providers/authentication/credentials/dispatch"; | ||
import { CredentialsAction } from "../../../../providers/authentication/credentials/types"; | ||
import { USER_PROVIDER_KEY } from "../../../../providers/authentication/profile/constants"; | ||
import { | ||
requestProfileError, | ||
requestProfileSuccess, | ||
resetProfile, | ||
} from "../../../../providers/authentication/profile/dispatch"; | ||
import { | ||
ProfileAction, | ||
UserProfile, | ||
} from "../../../../providers/authentication/profile/types"; | ||
import { getAuthenticationRequestOptions } from "../../../useAuthentication/common/utils"; | ||
import { fetchProfile } from "../../profile/utils"; | ||
import { OAuthResponse, OAuthRevokeResponse } from "./types"; | ||
|
||
/** | ||
* Throws an error if the provider is not configured. | ||
* @param provider - Provider. | ||
*/ | ||
export function assertOAuthProvider(provider?: unknown): void { | ||
if (isOAuthProvider(provider)) return; | ||
throw new Error( | ||
"Open Authentication does not have required token client configured." | ||
); | ||
} | ||
|
||
/** | ||
* Handles the OAuth request response. | ||
* - Dispatches request response. | ||
* @param dispatch - Credentials dispatch. | ||
* @param response - OAuth request response. | ||
*/ | ||
export function handleOAuthRequest( | ||
dispatch: Dispatch<CredentialsAction<string>> | null, | ||
response: OAuthResponse | ||
): void { | ||
const { access_token } = response; | ||
dispatch?.(requestCredentials(access_token)); | ||
} | ||
|
||
/** | ||
* Handles the OAuth request response. | ||
* - Fetches profile, and dispatches response. | ||
* @param dispatch - Profile dispatch. | ||
* @param response - OAuth request response. | ||
* @param endpoint - OAuth profile endpoint. | ||
* @param mapProfile - Function; maps profile response to user profile. | ||
*/ | ||
export function handleOAuthRequestProfile( | ||
dispatch: Dispatch<ProfileAction<UserProfile | undefined>> | null, | ||
response: OAuthResponse, | ||
endpoint: string, | ||
mapProfile: (r: unknown) => UserProfile | undefined | ||
): void { | ||
const { access_token } = response; | ||
fetchProfile(endpoint, getAuthenticationRequestOptions(access_token), { | ||
onError: () => dispatch?.(requestProfileError(USER_PROVIDER_KEY)), | ||
onSuccess: (r) => { | ||
const profile = mapProfile(r); | ||
dispatch?.(requestProfileSuccess(USER_PROVIDER_KEY, { profile })); | ||
}, | ||
}); | ||
} | ||
|
||
/** | ||
* Handles the OAuth revoke response. | ||
* - Dispatches revoked response. | ||
* @param dispatch - Credentials dispatch. | ||
* @param response - OAuth revoke response. | ||
*/ | ||
export function handleOAuthRevoke( | ||
dispatch: Dispatch<CredentialsAction<string>> | null, | ||
response: OAuthRevokeResponse | ||
): void { | ||
const { successful: isSuccess } = response; | ||
dispatch?.(revokeCredentials({ isSuccess })); | ||
} | ||
|
||
/** | ||
* Handles resetting the user profile. | ||
* - Dispatches reset profile. | ||
* @param dispatch - Profile dispatch. | ||
*/ | ||
export function handleResetProfile( | ||
dispatch: Dispatch<ProfileAction<UserProfile>> | null | ||
): void { | ||
dispatch?.(resetProfile(USER_PROVIDER_KEY)); | ||
} | ||
|
||
/** | ||
* Returns true if the provider is an OAuth provider. | ||
* @param provider - Provider. | ||
* @returns provider is OAuth provider. | ||
*/ | ||
export function isOAuthProvider( | ||
provider: unknown | ||
): provider is AuthProviderConfig { | ||
return (provider as AuthProviderConfig)?.oauth !== undefined; | ||
} |
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 @@ | ||
export const PROVIDER_KEY = "google"; |
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,16 @@ | ||
export interface Error { | ||
error: string; | ||
error_description: string; | ||
} | ||
|
||
export interface Response { | ||
email: string; | ||
email_verified: boolean; | ||
family_name: string; | ||
given_name: string; | ||
hd: string; | ||
locale: string; | ||
name: string; | ||
picture: string; | ||
sub: string; | ||
} |
64 changes: 64 additions & 0 deletions
64
src/hooks/authentication/oauth/google/useGoogleAuthentication.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,64 @@ | ||
import { useCallback } from "react"; | ||
import { useCredentials } from "../../../../providers/authentication/credentials/hook"; | ||
import { useProfile } from "../../../../providers/authentication/profile/hook"; | ||
import { UserProfile } from "../../../../providers/authentication/profile/types"; | ||
import { useAuthenticationConfig } from "../../../useAuthenticationConfig"; | ||
import { | ||
AuthenticationService, | ||
OAuthResponse, | ||
OAuthRevokeResponse, | ||
} from "../common/types"; | ||
import { | ||
assertOAuthProvider, | ||
handleOAuthRequest, | ||
handleOAuthRequestProfile, | ||
handleOAuthRevoke, | ||
handleResetProfile, | ||
} from "../common/utils"; | ||
import { PROVIDER_KEY } from "./constants"; | ||
import { mapProfile } from "./utils"; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544. | ||
declare const google: any; | ||
|
||
export const useGoogleAuthentication = (): AuthenticationService => { | ||
const { | ||
credentialsDispatch, | ||
credentialsState: { credentials }, | ||
} = useCredentials(); | ||
const { profileDispatch } = useProfile<UserProfile | undefined>(); | ||
const { provider: { [PROVIDER_KEY]: provider } = {} } = | ||
useAuthenticationConfig(); | ||
assertOAuthProvider(provider); | ||
|
||
const login = useCallback(() => { | ||
const client = google.accounts.oauth2.initTokenClient({ | ||
callback: (response: OAuthResponse) => { | ||
handleOAuthRequest(credentialsDispatch, response); | ||
handleOAuthRequestProfile( | ||
profileDispatch, | ||
response, | ||
provider.endpoint, | ||
mapProfile | ||
); | ||
}, | ||
...provider.oauth, | ||
}); | ||
client.requestAccessToken(); | ||
}, [credentialsDispatch, profileDispatch, provider]); | ||
|
||
const logout = useCallback(() => { | ||
google.accounts.oauth2.revoke( | ||
credentials, | ||
(response: OAuthRevokeResponse) => { | ||
handleOAuthRevoke(credentialsDispatch, response); | ||
handleResetProfile(profileDispatch); | ||
} | ||
); | ||
}, [credentials, credentialsDispatch, profileDispatch]); | ||
|
||
return { | ||
login, | ||
logout, | ||
}; | ||
}; |
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,42 @@ | ||
import { UserProfile } from "../../../../providers/authentication/profile/types"; | ||
import { Response } from "./types"; | ||
|
||
/** | ||
* Returns full name, from given and family name. | ||
* @param response - Google response. | ||
* @returns full name. | ||
*/ | ||
function getFullName(response: Response): string { | ||
const { family_name: lastName = "", given_name: firstName = "" } = response; | ||
return `${firstName} ${lastName}`.trim(); | ||
} | ||
|
||
/** | ||
* Returns true if response is a google profile. | ||
* @param response - Google response. | ||
* @returns response is google profile. | ||
*/ | ||
function isProfileGoogle(response: unknown): response is Response { | ||
return ( | ||
(response as Response)?.email !== undefined && | ||
(response as Response)?.family_name !== undefined && | ||
(response as Response)?.given_name !== undefined | ||
); | ||
} | ||
|
||
/** | ||
* Returns user profile from google response. | ||
* @param response - Google response. | ||
* @returns user profile. | ||
*/ | ||
export function mapProfile(response: unknown): UserProfile | undefined { | ||
if (isProfileGoogle(response)) { | ||
const { email, picture } = response; | ||
const fullName = getFullName(response); | ||
return { | ||
email, | ||
fullName, | ||
picture, | ||
}; | ||
} | ||
} |
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,4 @@ | ||
export interface ProfileHandler<R, E> { | ||
onError: (error: E) => void; | ||
onSuccess: (response: R) => void; | ||
} |
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,16 @@ | ||
import { ProfileHandler } from "./types"; | ||
|
||
export function fetchProfile<R, E>( | ||
endpoint: string, | ||
options?: RequestInit, | ||
handler?: ProfileHandler<R, E> | ||
): void { | ||
fetch(endpoint, options) | ||
.then((response) => response.json()) | ||
.then((r: R) => { | ||
handler?.onSuccess(r); | ||
}) | ||
.catch((e: E) => { | ||
handler?.onError(e); | ||
}); | ||
} |
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,38 @@ | ||
import { | ||
CredentialsState, | ||
RequestCredentialsPayload, | ||
RevokeCredentialsPayload, | ||
} from "./types"; | ||
|
||
/** | ||
* Request credentials action. | ||
* @param state - State. | ||
* @param payload - Payload. | ||
* @returns state. | ||
*/ | ||
export function requestCredentialsAction<C>( | ||
state: CredentialsState<C>, | ||
payload: RequestCredentialsPayload<C> | ||
): CredentialsState<C> { | ||
return { | ||
...state, | ||
credentials: payload, | ||
}; | ||
} | ||
|
||
/** | ||
* Revoke credentials action. | ||
* @param state - State. | ||
* @param payload - Payload. | ||
* @returns state. | ||
*/ | ||
export function revokeCredentialsAction<C>( | ||
state: CredentialsState<C>, | ||
payload: RevokeCredentialsPayload | ||
): CredentialsState<C> { | ||
if (!payload.isSuccess) throw new Error("Failed to revoke credentials."); | ||
return { | ||
...state, | ||
credentials: undefined, | ||
}; | ||
} |
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,10 @@ | ||
import { createContext } from "react"; | ||
import { CredentialsContextProps } from "./types"; | ||
import { initCredentials } from "./utils"; | ||
|
||
export const CredentialsContext = createContext< | ||
CredentialsContextProps<unknown> | ||
>({ | ||
credentialsDispatch: null, | ||
credentialsState: initCredentials<unknown>(), | ||
}); |
Oops, something went wrong.