Skip to content

Commit

Permalink
fix: properly redirect after login in nextauth-based authentication (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterckx committed Oct 29, 2024
1 parent fae5e30 commit 5885d17
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 7 deletions.
6 changes: 4 additions & 2 deletions app/components/Login/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
TermsOfService,
} from "@databiosphere/findable-ui/lib/components/Login/login.styles";
import { Checkbox, Typography } from "@mui/material";
import { ClientSafeProvider, signIn } from "next-auth/react";
import { ClientSafeProvider } from "next-auth/react";
import { ChangeEvent, ReactNode, useState } from "react";
import { useAuthentication } from "../../hooks/useAuthentication/useAuthentication";
import { NextAuthProviders } from "./common/entities";

export interface LoginProps {
Expand All @@ -36,6 +37,7 @@ export const Login = ({
}: LoginProps): JSX.Element => {
const [isError, setIsError] = useState<boolean>(false);
const [isInAgreement, setIsInAgreement] = useState<boolean>(!termsOfService);
const { authenticateUser } = useAuthentication();

// Authenticates the user, if the user has agreed to the terms of service.
// If the terms of service are not accepted, set the terms of service error state to true.
Expand All @@ -44,7 +46,7 @@ export const Login = ({
setIsError(true);
return;
}
signIn(provider.id);
authenticateUser(provider.id);
};

// Callback fired when the checkbox value is changed.
Expand Down
58 changes: 58 additions & 0 deletions app/hooks/useAuthenticationRedirectRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { escapeRegExp } from "@databiosphere/findable-ui/lib/common/utils";
import { INACTIVITY_PARAM } from "@databiosphere/findable-ui/lib/hooks/useSessionTimeout";
import { ROUTE_LOGIN } from "@databiosphere/findable-ui/lib/providers/authentication";
import { useRouter } from "next/router";
import { useMemo, useRef } from "react";

/**
* Handles the authentication redirect route.
* @returns route to redirect to when authentication is complete.
*/
export const useAuthenticationRedirectRoute = (): string => {
const { asPath } = useRouter();
const routeHistoryRef = useRef<string>(initRouteHistory(asPath));

// Maintain a history of routes that have been visited prior to authentication.
routeHistoryRef.current = useMemo(
() => updateRouteHistory(routeHistoryRef.current, asPath),
[asPath]
);

return routeHistoryRef.current;
};

/**
* Initializes route history with the current path.
* Returns base path if current path is the login route.
* @param path - current browser path.
* @returns path to be used as the initial route history.
*/
function initRouteHistory(path: string): string {
return path === ROUTE_LOGIN ? "/" : removeInactivityTimeoutQueryParam(path);
}

/**
* Removes the inactivity timeout query parameter from the path.
* the inactivity timeout parameter is used to indicate that the session has timed out; remove the parameter to
* clear the session timeout banner after the user logs in again.
* @param path - Path.
* @returns path without the inactivity timeout query parameter.
*/
function removeInactivityTimeoutQueryParam(path: string): string {
const regex = new RegExp(`\\?${escapeRegExp(INACTIVITY_PARAM)}(?:$|[=&].*)`);
return path.replace(regex, "");
}

/**
* Updates route history with the current path, unless the current path is the LoginView page.
* @param prevPath - route history path.
* @param path - current browser path.
* @returns updated path to be used as the route history.
*/
function updateRouteHistory(prevPath: string, path: string): string {
let currentPath = prevPath;
if (path !== ROUTE_LOGIN) {
currentPath = path;
}
return removeInactivityTimeoutQueryParam(currentPath);
}
23 changes: 18 additions & 5 deletions app/providers/authentication.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { AUTHENTICATION_STATUS } from "@databiosphere/findable-ui/lib/hooks/useAuthentication/common/entities";
import { useAuthenticationComplete } from "@databiosphere/findable-ui/lib/hooks/useAuthentication/useAuthenticationComplete";
import { useConfig } from "@databiosphere/findable-ui/lib/hooks/useConfig";
import { INACTIVITY_PARAM } from "@databiosphere/findable-ui/lib/hooks/useSessionTimeout";
import { Session } from "next-auth";
import { signOut, useSession } from "next-auth/react";
import { signIn, signOut, useSession } from "next-auth/react";
import Router, { useRouter } from "next/router";
import { createContext, ReactNode, useCallback } from "react";
import { useIdleTimer } from "react-idle-timer";
import { useAuthenticationRedirectRoute } from "../hooks/useAuthenticationRedirectRoute";

// Template constants
export const ROUTE_LOGIN = "/login";

type AuthenticateUserFn = (providerId: string) => void;
type RequestAuthenticationFn = () => void;

/**
* Model of authentication context.
*/
export interface AuthContextProps {
authenticateUser: AuthenticateUserFn;
authenticationStatus: AUTHENTICATION_STATUS;
isAuthenticated: boolean;
isEnabled: boolean;
Expand All @@ -28,6 +30,8 @@ export interface AuthContextProps {
* Auth context for storing and using auth-related state.
*/
export const AuthContext = createContext<AuthContextProps>({
// eslint-disable-next-line @typescript-eslint/no-empty-function -- allow dummy function for default state.
authenticateUser: () => {},
authenticationStatus: AUTHENTICATION_STATUS.INCOMPLETE,
isAuthenticated: false,
isEnabled: true,
Expand Down Expand Up @@ -58,9 +62,7 @@ export function AuthProvider({ children, sessionTimeout }: Props): JSX.Element {
const authenticationStatus = isAuthenticated
? AUTHENTICATION_STATUS.COMPLETED
: AUTHENTICATION_STATUS.INCOMPLETE;

// Handle completion of authentication process.
useAuthenticationComplete(authenticationStatus);
const redirectRoute = useAuthenticationRedirectRoute();

/**
* If sessionTimeout is set and user is authenticated, the app will reload and redirect to
Expand All @@ -81,6 +83,16 @@ export function AuthProvider({ children, sessionTimeout }: Props): JSX.Element {
timeout: sessionTimeout,
});

/**
* Authenticates user and redirects to previous page.
*/
const authenticateUser = useCallback(
(providerId: string): void => {
signIn(providerId, { callbackUrl: redirectRoute });
},
[redirectRoute]
);

/**
* Navigates to login page.
*/
Expand All @@ -91,6 +103,7 @@ export function AuthProvider({ children, sessionTimeout }: Props): JSX.Element {
return (
<AuthContext.Provider
value={{
authenticateUser,
authenticationStatus,
isAuthenticated,
isEnabled,
Expand Down

0 comments on commit 5885d17

Please sign in to comment.