Auth

This document provides a detailed overview of the Intecode Common Auth Module, which is responsible for handling user authentication and authorization.

Functionality

The Auth module ensures secure access to the system by managing user login, registration, password recovery, and session handling. It includes role-based access controls and integrates with OAuth providers.

Auth Provider

The AuthProvider component and AuthContext provide authentication management and user context for the application. It handles storing and managing authentication tokens, user information, and team-related updates.

Key Features:

1. State Management:

  • token & refreshToken: Stored in localStorage using the useLocalStorage hook. These hold the access token and refresh token for authenticated sessions.

  • currentUser: Holds the currently authenticated user's data.

2. Functions:

  • fetchUser:

    • Fetches the current user data from the API using userService.getUser().

    • Updates user roles and checks if the user has a team. If no team exists, it navigates to the team creation page.

  • setAuth: Stores the access token and refresh token in localStorage.

  • logout:

    • Logs the user out by deactivating their membership (if applicable) and clears the authentication tokens.

    • Redirects to the sign-in page.

  • changeTeam:

    • Switches the active team by calling the authService.changeTeam API and updating tokens.

    • Re-fetches the user data after changing the team.

3. Context Usage:

  • AuthContext.Provider: Provides the authentication-related values and functions (token, setAuth, logout, etc.) to the component tree.

  • useAuth: Hook to access the AuthContext values and functions within any component.

Usage Example:

To access the authentication context:

const { token, logout, fetchUser, currentUser } = useAuth();

This structure allows centralized control of authentication, token management, and user/team updates throughout the app.

AuthContext.js
import { createContext, useCallback, useContext, useState } from 'react';
import { useLocalStorage } from '../common/hooks/useLocalStorage';
import { useNavigate } from 'react-router-dom';
import { useUserService } from '../services/userService';
import { REFRESH_TOKEN, TOKEN, USER_ROLES } from '../common/constants/localStorage';
import { updateToken } from '../services/tokenService';
import { useAuthService } from '../services/authService';
import globalNotification from '../common/hooks/globalNotification';
import { useTeamService } from '../services/teamService';

export const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
    const [token, setToken] = useLocalStorage(TOKEN, null);
    const [refreshToken, setRefreshToken] = useLocalStorage(REFRESH_TOKEN, null);
    const [currentUser, setCurrentUser] = useState();
    const [isUserLoading, setIsUserLoading] = useState(true);

    const navigate = useNavigate();
    const userService = useUserService();
    const authService = useAuthService();
    const { updateMembership, findUserMembership } = useTeamService();

    const fetchUser = useCallback(async () => {
        try {
            if (token) {
                setIsUserLoading(true);
                const { data } = await userService.getUser();

                setCurrentUser(data);
                updateToken(USER_ROLES, data.userRoleNames);
                setTimeout(() => {
                    setIsUserLoading(false);
                }, 1000);

                if (!data.teamId) {
                    navigate('/create-team');
                }
            }
        } catch (e) {
            logout();
            console.error(e);
        }
    }, []);

    const setAuth = (token, refreshToken) => {
        setToken(token);
        setRefreshToken(refreshToken);
    };

    const logout = async () => {
        try {
            if (!currentUser) {
                throw new Error();
            }

            const { data } = await findUserMembership(currentUser.id);

            if (!data) {
                throw new Error();
            }

            await updateMembership({ id: data.id, isActive: false });
        } catch (e) {
            console.log(e);
        } finally {
            updateToken(TOKEN, null);
            updateToken(REFRESH_TOKEN, null);
            updateToken(USER_ROLES, null);
            navigate('/auth/sign-in');
        }
    };

    const changeTeam = async teamId => {
        try {
            const { data } = await authService.changeTeam(teamId);

            setAuth(data.accessToken, data.refreshToken || data.accessToken);
        } catch (error) {
            globalNotification.open({ message: 'This membership is not valid' });
        } finally {
            await fetchUser();
        }
    };

    return (
        <AuthContext.Provider
            value={{
                token,
                setAuth,
                logout,
                fetchUser,
                currentUser,
                isUserLoading,
                changeTeam,
            }}
        >
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => {
    return useContext(AuthContext);
};

Components

Components Necessary for Optimal Functioning of the Auth Module

Auth Guard

The AuthGuard component is a route protection mechanism that checks if a user is authenticated before granting access to specific routes.

Key Features:

  1. Authentication Check:

    • It verifies if the user is on an "auth" route (e.g., /auth/sign-in) by checking the URL path.

    • If the user is already authenticated (i.e., has valid tokens stored), they are redirected to the dashboard route.

  2. Route Protection:

    • If the user is accessing a non-auth route and doesn't have valid tokens (TOKEN and REFRESH_TOKEN in localStorage), they are redirected to the sign-in page (auth/sign-in).

import { useLocation } from 'react-router-dom';
import { Navigate } from 'react-router-dom';
import { routesPath } from '../../common/constants/routesPath';
import { TOKEN, REFRESH_TOKEN } from '../../common/constants/localStorage';

function AuthGuard({ children }) {
    const location = useLocation();
    const isAuth = location.pathname.split('/')[1] === 'auth';
    if (isAuth) {
        return getToken(TOKEN) && getToken(REFRESH_TOKEN) ? <Navigate to={routesPath.ROOT.DASHBOARD} /> : children;
    } else {
        return getToken(TOKEN) && getToken(REFRESH_TOKEN) ? children : <Navigate to={'auth/sign-in'} />;
    }
}

const getToken = tokenName => {
    const token = localStorage.getItem(tokenName);

    if (token === null || token === 'undefined') {
        return null;
    }
    return JSON.parse(localStorage.getItem(tokenName));
};

export default AuthGuard;

GetUser Decorator

This decorator retrieves user data parsed by JwtAuthenticationGuard

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { JwtUserPayload } from 'src/common/interfaces/jwt-user-payload.interface';

export const GetUser = createParamDecorator((data, ctx: ExecutionContext): JwtUserPayload => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
});

Auth Flow Schema

Last updated