Поиск  
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)

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

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

Похожее
Mar 19, 2021
Author: Mukesh Murugan
In this article, We will talk about Onion Architecture In ASP.NET Core and it’s advantages. We will also together build a WebApi that follows a variant of Onion Architecture so that we get to see why it is important to...
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...
Jul 19
Author: Anton Skuratovich
Microsoft has recently introduced .NET Aspire, an opinionated stack for building resilient, observable, and configurable cloud-native applications with .NET 8+. Delivered through a collection of NuGet packages, .NET Aspire addresses specific cloud-native concerns, catering to the intricacies of modern, cloud-centric...
Nov 17, 2020
RSS stands for "Really Simple Syndication". It is a web feed mechanism, which helps applications or clients get updated content from websites in a timely fashion and empower applications to get content from more than one website regularly without having...
Написать сообщение
Тип
Почта
Имя
*Сообщение