Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jun 1

Web API and React with Integration Testing (Part 3: React Frontend)

Автор:
Источник:
Просмотров:
1540

The final part of our series brings the user interface to life, integrating our secure ASP.NET 8 Web API with a React and Redux frontend. In this article, we’ll cover how to manage authentication states, securely store and handle authentication tokens, and communicate with the backend API from your React application. We’ll also delve into best practices for protecting routes and managing user sessions in a SPA (Single Page Application) context. By the end of this guide, you’ll have a fully functional, secure web application with a modern, reactive frontend that seamlessly interacts with your ASP.NET backend. This part is essential for developers looking to bridge the gap between backend security and frontend user experience.

Part 1: Setting up API
Part 2: Setting up integration tests for API project
Part 3: Setting up React client

Web project: React frontend

The web project is created with Vite. We are using ReduxJS Toolkit for state management, Ant Design (AntD) for frontend design, and Axios for API call management.

Visual Studio 2022 provides a new JavaScript project type, and we are utilizing that for our project.

Javascript project type for Visual Studio
Javascript project type for Visual Studio

Let’s go through the most important files to understand how the project is wired up.

Web project files

In our application, the entry point is main.tsx, where we configure fundamental aspects such as Redux state management, client-side routing using React Router, and an Axios API interceptor. This file initializes the React app, wraps it in StrictMode for enhanced development checks, and renders the main ‘App’ component within the specified ‘root’ element in the HTML document.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { store } from "./app/store.ts";
import { AxiosApiInterceptor } from "./app/AxiosApiInterceptor.ts";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <AxiosApiInterceptor />
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>
);

store.ts sets up the Redux store configuration for a React application using the @reduxjs/toolkit library. It defines an authentication reducer (authReducer) and combines it with state persistence using redux-persist. The configureStore function creates the Redux store, enabling DevTools during development. The persistStore function is used to create a persisted version of the store, ensuring that the authentication state persists across page reloads. Additionally, type definitions (AppDispatch, RootState, and AppThunk) are provided for consistent use throughout the application. Overall, this configuration facilitates efficient state management, asynchronous actions, and persistent storage for the authentication slice of the Redux store.

import {
  configureStore,
  ThunkAction,
  Action,
  combineReducers,
} from "@reduxjs/toolkit";
import authReducer from "../features/user/authSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore } from "redux-persist";
import thunk from "redux-thunk";

const authPersistConfig = {
  key: "auth",
  storage:storage,
};

const rootReducer = combineReducers({
  auth: persistReducer(authPersistConfig, authReducer),
});

export const store = configureStore({
  reducer: rootReducer,
  devTools: process.env.NODE_ENV !== "production",
  middleware: [thunk],
});

export const persister = persistStore(store);

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

authAPI.ts defines a set of functions for making API requests related to user authentication in a React application. Utilizing Axios, the login, refreshToken, register, logout, and profileApi functions interact with a backend server at a specified base URL. Each function handles its respective endpoint, such as user login, token refresh, registration, logout, and fetching user profile. In case of any errors during API requests, the code logs the exceptions to the console. This collection of functions demonstrates a straightforward and modular approach to integrating user authentication API calls in a React application.

import axios from "axios";
import { iAppResponse } from "../../app/appResponse";

const BASE_URL = "https://localhost:1002";

export const login = async (email: string, password: string) => {
  const response = await axios.post<
  iAppResponse<{ accessToken: string; refreshToken: string }>
  >(`${BASE_URL}/user/login`, {
    email: email,
    password: password,
  }).catch((ex)=>{
    console.log(ex);
  });
  return response?.data;
};
export const refreshToken = async (data: {
  accessToken: string;
  refreshToken: string;
}) => {
  const response = await axios.post<
  iAppResponse<{ accessToken: string; refreshToken: string }>
  >(`${BASE_URL}/user/refreshToken`, data).catch((ex)=>{
    console.log(ex);
  });;
  return response?.data;
};
export const register = async (email: string, password: string) => {
  const response = await axios.post<iAppResponse<{}>>(
    `${BASE_URL}/user/register`,
    {
      email: email,
      password: password,
    }
  ).catch((ex)=>{
    console.log(ex);
  });
  return response?.data;
};
export const logout = async () => {
  const response = await axios.post<iAppResponse<boolean>>(
    `${BASE_URL}/user/logout`
  ).catch((ex)=>{
    console.log(ex);
  });;
  return response?.data;
};
export const profileApi = async () => {
  const response = await axios.post(`${BASE_URL}/user/profile`).catch((ex)=>{
    console.log(ex);
  });
  return response?.data;
};

authSlice.ts for managing authentication-related state in the application. It includes a set of reducers to update or reset authentication tokens, manage loading states, and handle logout asynchronously. Overall, this slice facilitates the centralized management of authentication-related data, making it easier to handle user authentication and related asynchronous actions within the Redux store.

import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { jwtDecode } from "jwt-decode";
import { logout } from "./authAPI";

export interface iUser {
  Id: string;
  RoleClaim: Array<string>;
  UserName: string;
}
export interface iAuthState {
  status: "idle" | "loading" | "failed";
  accessToken?: string;
  refreshToken?: string;
  user?: iUser;
}

const initialState: iAuthState = {
  status: "idle",
};
export const logoutAsync = createAsyncThunk("user/logout", async () => {
  const response = await logout();
  // The value we return becomes the `fulfilled` action payload
  return response?.data;
});
export const authSlice = createSlice({
  name: "auth",
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    updateToken: (
      state,
      action: PayloadAction<{ accessToken: string; refreshToken: string }>
    ) => {
      state.accessToken = action.payload.accessToken;
      state.refreshToken = action.payload.refreshToken;
      state.user = jwtDecode<iUser>(action.payload.accessToken);
    },
    resetToken: (state) => {
      state.accessToken = undefined;
      state.refreshToken = undefined;
      state.user = undefined;
    },
    setLoading: (state) => {
      state.status = "loading";
    },
    resetLoading: (state) => {
      state.status = "idle";
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(logoutAsync.pending, (state) => {
        state.status = "loading";
      })
      .addCase(logoutAsync.fulfilled, (state) => {
        state.status = "idle";
        state.accessToken = undefined;
        state.refreshToken = undefined;
        state.user = undefined;
      })
      .addCase(logoutAsync.rejected, (state) => {
        state.status = "failed";
      });
  },
});

export const { updateToken, resetToken, setLoading, resetLoading } = authSlice.actions;
export const selectAuth = (state: RootState) => state.auth;
export default authSlice.reducer;

AxiosApiInterceptor.ts component is a crucial part of the React application, utilizing Axios interceptors to manage authentication tokens. When a request is made, it ensures the inclusion of the access token in the Authorization header, improving security. Additionally, it monitors responses, particularly handling cases where a 401 status code indicates an expired token. In such situations, it attempts to refresh the token using a provided function. If the refresh is successful, the Redux store is updated with the new tokens, allowing the original request to be retried. If the refresh fails, or if no refresh token is available, the authentication state is reset. This component helps maintain a seamless and secure user authentication experience throughout the application.

import { useEffect } from "react";
import axios from "axios";
import {
  resetToken,
  selectAuth,
  updateToken,
} from "../features/user/authSlice";
import { useAppDispatch, useAppSelector } from "./hooks";
import { refreshToken } from "../features/user/authAPI";

export const AxiosApiInterceptor = () => {
  const authData = useAppSelector(selectAuth);
  const dispatch = useAppDispatch();
  useEffect(() => {
    const requestInterceptor = axios.interceptors.request.use(
      async (config) => {
        const accessToken = authData.accessToken;
        if (accessToken && !config.headers.Authorization) {
          config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
      }
    );

    // Response interceptor
    const responseInterceptor = axios.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error.response && error.response.status === 401) {
          // Token expired, attempt to refresh it
          if (authData.refreshToken && authData.accessToken) {
            // Make a refresh token request and update access token
            try {
              const response = await refreshToken({
                accessToken: authData.accessToken,
                refreshToken: authData.refreshToken,
              });
              if (response && response.isSucceed && response.data) {
                dispatch(updateToken(response.data));
                error.config.headers.Authorization = `Bearer ${response.data.accessToken}`;
                return axios.request(error.config);
              } else {
                dispatch(resetToken());
              }
            } catch (refreshError) {
              dispatch(resetToken());
              throw refreshError;
            }
          } else {
            dispatch(resetToken());
          }
        }

        return Promise.reject(error);
      }
    );

    return () => {
      // Cleanup: Remove the interceptors when the component unmounts
      axios.interceptors.request.eject(requestInterceptor);
      axios.interceptors.response.eject(responseInterceptor);
    };
  }, [authData, dispatch]);

  return null; // This component doesn't render anything
};

The remaining components are straightforward, and I trust you’ll find them self-explanatory. If you have any further inquiries or need clarification, please feel free to ask.

Summary

This code module serves as a comprehensive guide to integrating a React frontend with an authentication API. Focused on practical implementation, it employs Axios for API requests and demonstrates key functionalities such as user login, token refresh, registration, logout, and fetching user profile. The code promotes a modular and error-handling approach, logging exceptions to the console during API requests. This collection of functions showcases a robust integration of user authentication features within a React application, enhancing practicality and reliability.

Source Code

For the complete code and more examples, check out the GitHub repository: UnifiedApp

Похожее
Sep 23, 2022
Author: Jaydeep Patil
In this article, we will discuss gRPC and perform CRUD Operation using that and step-by-step implementation of gRPC. We take Product Application here to understand how things are going to work with gRPC and, in that first, we create ProductOfferGrpcService...
Jul 22
Author: Ankit Sahu
Introduction In this article, we are going to discuss What Entity Framework is and How we can implement it in the .Net 8 project. This is a continuation of part 1, so if you are new to this article, please...
Apr 24, 2022
Author: HungryWolf
What is MediatR? Why do we need it? And How to use it? Mediator Pattern - The mediator pattern ensures that objects do not interact directly instead of through a mediator. It reduces coupling between objects which makes it easy...
Feb 17, 2023
Author: Juldhais Hengkyawan
A Guide to Building Scalable, Maintainable Web API using ASP .NET Core The term “Clean Architecture” has become increasingly popular in software development in recent years. Clean Architecture is a software design pattern that prioritizes the separation of concerns, making...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
Семь итераций наивности или как я полтора года свою дебютную игру писал
Почему сеньоры ненавидят собеседования с кодингом, и что компании должны использовать вместо них
Дюжина логических задач с собеседований
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
Жестокая правда будней программиста: 8 наблюдений
Стили именования переменных и функций. Используйте их все
Чем CRM отличается от ERP
Что должен уметь Frontend разработчик в 2023 году (+Roadmap)
Компиляция и запуск C# и Blazor внутри браузера
Функции и хранимые процедуры в PostgreSQL: зачем нужны и как применять в реальных примерах
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile