Skip to content

Commit

Permalink
feat: added session controller to google sign in for non-terra relate…
Browse files Browse the repository at this point in the history
…d authentication (#178)
  • Loading branch information
Fran McDade authored and Fran McDade committed Jan 5, 2025
1 parent a383043 commit 05fa1ed
Show file tree
Hide file tree
Showing 16 changed files with 180 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Fragment, useEffect } from "react";
import { authComplete } from "../../../../../../providers/authentication/auth/dispatch";
import { useAuth } from "../../../../../../providers/authentication/auth/hook";
import { authenticationComplete } from "../../../../../../providers/authentication/authentication/dispatch";
import { useAuthentication } from "../../../../../../providers/authentication/authentication/hook";
import { updateCredentials } from "../../../../../../providers/authentication/credentials/dispatch";
import { useCredentials } from "../../../../../../providers/authentication/credentials/hook";
import { SessionControllerProps } from "./types";

export function SessionController({
children,
token,
}: SessionControllerProps): JSX.Element {
const { authDispatch } = useAuth();
const {
authenticationDispatch,
authenticationState: { profile },
} = useAuthentication();
const { credentialsDispatch } = useCredentials();

useEffect(() => {
// Dispatch only when profile is available:
// - Login errors are managed by the login service.
// - Logout operations handle resetting credentials and authentication state.
if (!profile) return;
credentialsDispatch?.(updateCredentials(token)); // Release credentials.
authenticationDispatch?.(authenticationComplete()); // Authentication `status` is "SETTLED".
authDispatch?.(authComplete({ isAuthenticated: true })); // Auth `status` is "SETTLED", and `isAuthenticated` is "true".
}, [
authDispatch,
authenticationDispatch,
credentialsDispatch,
profile,
token,
]);

return <Fragment>{children}</Fragment>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReactNode } from "react";
import { TokenState } from "../../../../../../providers/authentication/token/types";

export interface SessionControllerProps {
children: ReactNode | ReactNode[];
token: TokenState["token"];
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSession } from "next-auth/react";
import React, { Fragment, useEffect } from "react";
import { updateAuthentication } from "../../../../providers/authentication/authentication/dispatch";
import { useAuthentication } from "../../../../providers/authentication/authentication/hook";
import { updateAuthentication } from "../../../../../../providers/authentication/authentication/dispatch";
import { useAuthentication } from "../../../../../../providers/authentication/authentication/hook";
import { SessionControllerProps } from "./types";
import { mapAuthentication } from "./utils";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
AUTHENTICATION_STATUS,
UpdateAuthenticationPayload,
UserProfile,
} from "../../../../providers/authentication/authentication/types";
} from "../../../../../../providers/authentication/authentication/types";

/**
* Returns the authentication profile and status from the session context.
Expand Down
20 changes: 16 additions & 4 deletions src/hooks/authentication/session/useSessionActive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ import {
AUTH_STATUS,
AuthState,
} from "../../../providers/authentication/auth/types";
import {
AUTHENTICATION_STATUS,
AuthenticationState,
} from "../../../providers/authentication/authentication/types";
import { ROUTE } from "../../../routes/constants";
import { useRouteHistory } from "../../useRouteHistory";
import { INACTIVITY_PARAM } from "./useSessionTimeout";

export const useSessionActive = (authState: AuthState): void => {
const { status } = authState;
export const useSessionActive = (
authState: AuthState,
authenticationState: AuthenticationState
): void => {
const { status: authStatus } = authState;
const { status: authenticationStatus } = authenticationState;
const { callbackUrl } = useRouteHistory(2);
const isReady =
authenticationStatus === AUTHENTICATION_STATUS.SETTLED &&
authStatus === AUTH_STATUS.SETTLED;

useEffect(() => {
if (status !== AUTH_STATUS.SETTLED) return;
if (!isReady) return;
Router.push(callbackUrl(transformRoute));
}, [callbackUrl, status]);
}, [callbackUrl, isReady]);
};

/**
Expand Down
27 changes: 27 additions & 0 deletions src/providers/authentication/auth/dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import {
AUTH_STATUS,
AuthActionKind,
RequestAuthAction,
ResetStateAction,
UpdateAuthStateAction,
UpdateAuthStatePayload,
} from "./types";

/**
* Auth is complete.
* @param payload - Payload.
* @returns Action.
*/
export function authComplete(
payload: UpdateAuthStatePayload
): UpdateAuthStateAction {
return {
payload: { ...payload, status: AUTH_STATUS.SETTLED },
type: AuthActionKind.UpdateAuthState,
};
}

/**
* Request auth action.
* @returns Action.
*/
export function requestAuth(): RequestAuthAction {
return {
payload: undefined,
type: AuthActionKind.RequestAuth,
};
}

/**
* Reset state action.
* @returns state.
Expand Down
5 changes: 4 additions & 1 deletion src/providers/authentication/auth/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { updateAuthState } from "./actions";
import { AuthAction, AuthActionKind, AuthState } from "./types";
import { AUTH_STATUS, AuthAction, AuthActionKind, AuthState } from "./types";

/**
* Auth reducer.
Expand All @@ -10,6 +10,9 @@ import { AuthAction, AuthActionKind, AuthState } from "./types";
export function authReducer(state: AuthState, action: AuthAction): AuthState {
const { payload, type } = action;
switch (type) {
case AuthActionKind.RequestAuth: {
return { ...state, status: AUTH_STATUS.PENDING };
}
case AuthActionKind.ResetState: {
return { ...state, ...state.initialState };
}
Expand Down
13 changes: 12 additions & 1 deletion src/providers/authentication/auth/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Dispatch } from "react";
import { ProviderId } from "../common/types";

export type AuthAction = ResetStateAction | UpdateAuthStateAction;
export type AuthAction =
| RequestAuthAction
| ResetStateAction
| UpdateAuthStateAction;

export enum AuthActionKind {
RequestAuth = "REQUEST_AUTH",
ResetState = "RESET_STATE",
UpdateAuthState = "UPDATE_AUTH_STATE",
}
Expand All @@ -20,6 +24,13 @@ export interface AuthState {
status: AUTH_STATUS;
}

export type RequestAuthAction = {
payload: RequestAuthPayload;
type: AuthActionKind.RequestAuth;
};

export type RequestAuthPayload = undefined;

export type ResetStateAction = {
payload: ResetStatePayload;
type: AuthActionKind.ResetState;
Expand Down
3 changes: 3 additions & 0 deletions src/providers/googleSignInAuthentication/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dispatch } from "react";
import { AuthAction, AuthContextProps } from "../../authentication/auth/types";
import {
AuthenticationAction,
AuthenticationContextProps,
Expand All @@ -14,11 +15,13 @@ import {

export interface SessionReducer {
authenticationReducer: AuthenticationContextProps;
authReducer: Omit<AuthContextProps, "service">;
credentialsReducer: CredentialsContextProps;
tokenReducer: TokenContextProps;
}

export interface SessionDispatch {
authDispatch: Dispatch<AuthAction> | null;
authenticationDispatch: Dispatch<AuthenticationAction> | null;
credentialsDispatch: Dispatch<CredentialsAction> | null;
tokenDispatch: Dispatch<TokenAction> | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const useGoogleSignInService = (reducer: SessionReducer): Service => {
const { findProvider } = useProviders();
const {
authenticationReducer: { authenticationDispatch },
authReducer: { authDispatch },
credentialsReducer: { credentialsDispatch },
tokenReducer: { tokenDispatch },
} = reducer;
Expand All @@ -18,22 +19,27 @@ export const useGoogleSignInService = (reducer: SessionReducer): Service => {
(providerId: ProviderId) => {
const provider = findProvider(providerId);
if (!provider) return;
service.login(provider, { authenticationDispatch, tokenDispatch });
service.login(provider, {
authDispatch,
authenticationDispatch,
tokenDispatch,
});
},
[authenticationDispatch, findProvider, tokenDispatch]
[authDispatch, authenticationDispatch, findProvider, tokenDispatch]
);

const onLogout = useCallback(
(options?: { callbackUrl?: string }) => {
service.logout({
authDispatch,
authenticationDispatch,
credentialsDispatch,
tokenDispatch,
});
if (!options?.callbackUrl) return;
Router.push(options?.callbackUrl).catch((e) => console.error(e));
},
[authenticationDispatch, credentialsDispatch, tokenDispatch]
[authDispatch, authenticationDispatch, credentialsDispatch, tokenDispatch]
);

return { requestLogin: onLogin, requestLogout: onLogout };
Expand Down
13 changes: 7 additions & 6 deletions src/providers/googleSignInAuthentication/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import { SessionController as DefaultSessionController } from "../../components/Authentication/components/SessionController/components/GoogleSessionController/SessionController";
import { useAuthReducer } from "../../hooks/authentication/auth/useAuthReducer";
import { useAuthenticationReducer } from "../../hooks/authentication/authentication/useAuthenticationReducer";
import { useCredentialsReducer } from "../../hooks/authentication/credentials/useCredentialsReducer";
import { useSessionActive } from "../../hooks/authentication/session/useSessionActive";
import { useSessionAuth } from "../../hooks/authentication/session/useSessionAuth";
import { useSessionCallbackUrl } from "../../hooks/authentication/session/useSessionCallbackUrl";
import { useSessionIdleTimer } from "../../hooks/authentication/session/useSessionIdleTimer";
import { useTokenReducer } from "../../hooks/authentication/token/useTokenReducer";
Expand All @@ -15,36 +15,37 @@ import { useGoogleSignInService } from "./hooks/useGoogleSignInService";
import { GoogleSignInAuthenticationProviderProps } from "./types";

export function GoogleSignInAuthenticationProvider({
APIServicesProvider,
children,
SessionController = DefaultSessionController,
timeout,
}: GoogleSignInAuthenticationProviderProps): JSX.Element {
const authReducer = useAuthReducer(AUTH_STATE);
const authenticationReducer = useAuthenticationReducer(AUTHENTICATION_STATE);
const credentialsReducer = useCredentialsReducer();
const tokenReducer = useTokenReducer(); // Reducer, local to Google Sign-In process only.
const service = useGoogleSignInService({
authReducer,
authenticationReducer,
credentialsReducer,
tokenReducer,
});
const { callbackUrl } = useSessionCallbackUrl();
const { authDispatch, authState } = authReducer;
const { isAuthenticated } = authState;
useSessionActive(authState);
const { authenticationState } = authenticationReducer;
useSessionActive(authState, authenticationState);
useSessionIdleTimer({
disabled: !isAuthenticated,
onIdle: () => service.requestLogout({ callbackUrl }),
timeout,
});
useSessionAuth({ authReducer, authenticationReducer });
return (
<CredentialsContext.Provider value={credentialsReducer}>
<AuthenticationContext.Provider value={authenticationReducer}>
<AuthContext.Provider value={{ authDispatch, authState, service }}>
<APIServicesProvider token={tokenReducer.tokenState.token}>
<SessionController token={tokenReducer.tokenState.token}>
{children}
</APIServicesProvider>
</SessionController>
</AuthContext.Provider>
</AuthenticationContext.Provider>
</CredentialsContext.Provider>
Expand Down
17 changes: 13 additions & 4 deletions src/providers/googleSignInAuthentication/service/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OAuthProvider } from "../../../config/entities";
import { requestAuth, resetState } from "../../authentication/auth/dispatch";
import {
requestAuthentication,
resetState as resetAuthenticationState,
Expand All @@ -21,27 +22,34 @@ declare const google: any;
export const service = {
login: (
provider: OAuthProvider,
dispatch: Pick<SessionDispatch, "authenticationDispatch" | "tokenDispatch">
dispatch: Pick<
SessionDispatch,
"authDispatch" | "authenticationDispatch" | "tokenDispatch"
>
): void => {
const client = google.accounts.oauth2.initTokenClient({
callback: (response: TokenSetParameters) => {
const { id, profile, userinfo } = provider;
const { access_token: token } = response;
dispatch.authDispatch?.(requestAuth());
dispatch.authenticationDispatch?.(requestAuthentication());
dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
onError: () =>
onError: () => {
dispatch.authDispatch?.(resetState());
dispatch.authenticationDispatch?.(
updateAuthentication({
profile: undefined,
status: AUTHENTICATION_STATUS.SETTLED,
})
),
);
dispatch.tokenDispatch?.(resetTokenState());
},
onSuccess: (r: GoogleProfile) =>
dispatch.authenticationDispatch?.(
updateAuthentication({
profile: profile(r),
status: AUTHENTICATION_STATUS.PENDING, // Authentication is pending until Terra profile status is resolved.
status: AUTHENTICATION_STATUS.PENDING, // Authentication is pending until session controller is resolved.
})
),
});
Expand All @@ -52,6 +60,7 @@ export const service = {
client.requestAccessToken();
},
logout: (dispatch: SessionDispatch): void => {
dispatch.authDispatch?.(resetState());
dispatch.authenticationDispatch?.(resetAuthenticationState());
dispatch.credentialsDispatch?.(resetCredentialsState());
dispatch.tokenDispatch?.(resetTokenState());
Expand Down
2 changes: 1 addition & 1 deletion src/providers/googleSignInAuthentication/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ElementType, ReactNode } from "react";

export interface GoogleSignInAuthenticationProviderProps {
APIServicesProvider: ElementType;
children: ReactNode | ReactNode[];
SessionController?: ElementType;
timeout?: number;
}
2 changes: 1 addition & 1 deletion src/providers/nextAuthAuthentication/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SessionProvider } from "next-auth/react";
import React from "react";
import { SessionController } from "../../components/Authentication/components/SessionController/SessionController";
import { SessionController } from "../../components/Authentication/components/SessionController/components/NextSessionController/SessionController";
import { useAuthReducer } from "../../hooks/authentication/auth/useAuthReducer";
import { useAuthenticationReducer } from "../../hooks/authentication/authentication/useAuthenticationReducer";
import { useSessionAuth } from "../../hooks/authentication/session/useSessionAuth";
Expand Down
Loading

0 comments on commit 05fa1ed

Please sign in to comment.