Monday, May 5, 2025

REACT: User Authentication & RBAC Integration

 

Models and controllers, you will modify the React/TypeScript frontend to properly integrate with your authentication system.


1. Updated Auth Models (src/models/Auth.ts)


// Reflects your Java User and Role entities
export interface Role {
  roleId: number;
  roleName: string;
}

export interface User {
  userId: number;
  userName: string;
  isActive: boolean;
  roleId: number;
  role: Role;
  // Note: password is excluded as it shouldn't be stored in frontend
}

export interface LoginCredentials {
  userName: string;
  password: string;
}

export interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

// API Response structure to match your backend
export interface APIResponse<T> {
  status: number;
  message: string;
  data: T;
  timestamp: string;
}


2. Updated Auth Service (src/services/authService.ts)


import axios from 'axios';
import { LoginCredentials, User, APIResponse } from '../models/Auth';

const API_URL = 'http://localhost:9090/api';

export const authService = {
  async login(credentials: LoginCredentials): Promise<{ user: User; token: string }> {
    // Matches your UserController endpoint
    const response = await axios.get<APIResponse<User>>(
      `${API_URL}/users/${credentials.userName}&${credentials.password}`
    );
    
    if (response.data.status !== 200) {
      throw new Error(response.data.message);
    }

    // In a real app, your backend should return a token
    // For now, we'll simulate one
    const token = `fake-jwt-token-for-${credentials.userName}`;
    return { user: response.data.data, token };
  },

  // Add methods to match your API
  async validateToken(token: string): Promise<User> {
    // This would call your JWT verification endpoint
    const response = await axios.get<APIResponse<User>>(`${API_URL}/employees`, {
      headers: { Authorization: token }
    });
    return response.data.data;
  },

  logout(): Promise<void> {
    return Promise.resolve();
  }
};


3. Enhanced Auth Context (src/contexts/AuthContext.tsx)


import React, { createContext, useContext, useEffect, useState } from 'react';
import { authService } from '../services/authService';
import { User, AuthState, LoginCredentials } from '../models/Auth';
import { useNavigate } from 'react-router-dom';

interface AuthContextType {
  authState: AuthState;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
  hasPermission: (requiredRole: string) => boolean;
  isOwner: (userId: number) => boolean;
}

const AuthContext = createContext<AuthContextType>({} as AuthContextType);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [authState, setAuthState] = useState<AuthState>({
    user: null,
    token: null,
    isAuthenticated: false,
    isLoading: true,
    error: null,
  });

  const navigate = useNavigate();

  useEffect(() => {
    const token = localStorage.getItem('token');
    if (token) {
      authService.validateToken(token)
        .then(user => {
          setAuthState({
            user,
            token,
            isAuthenticated: true,
            isLoading: false,
            error: null,
          });
        })
        .catch(() => {
          localStorage.removeItem('token');
          setAuthState(prev => ({
            ...prev,
            isLoading: false,
            isAuthenticated: false,
          }));
        });
    } else {
      setAuthState(prev => ({ ...prev, isLoading: false }));
    }
  }, []);

  const login = async (credentials: LoginCredentials) => {
    setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
    
    try {
      const { user, token } = await authService.login(credentials);
      
      localStorage.setItem('token', token);
      setAuthState({
        user,
        token,
        isAuthenticated: true,
        isLoading: false,
        error: null,
      });
      
      navigate(user.role.roleName === 'ADMIN' ? '/admin' : '/dashboard');
    } catch (error) {
      setAuthState({
        user: null,
        token: null,
        isAuthenticated: false,
        isLoading: false,
        error: error.message,
      });
      throw error;
    }
  };

  const logout = () => {
    authService.logout();
    localStorage.removeItem('token');
    setAuthState({
      user: null,
      token: null,
      isAuthenticated: false,
      isLoading: false,
      error: null,
    });
    navigate('/login');
  };

  const hasPermission = (requiredRole: string): boolean => {
    if (!authState.user) return false;
    return authState.user.role.roleName === requiredRole;
  };

  const isOwner = (userId: number): boolean => {
    return authState.user?.userId === userId;
  };

  return (
    <AuthContext.Provider 
      value={{ authState, login, logout, hasPermission, isOwner }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);


4. Updated ProtectedRoute (src/components/ProtectedRoute.tsx)



import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

interface ProtectedRouteProps {
  requiredRole?: string;
  ownerOnly?: boolean;
  ownerId?: number;
  redirectTo?: string;
}

const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ 
  requiredRole, 
  ownerOnly = false,
  ownerId,
  redirectTo = '/login' 
}) => {
  const { authState, hasPermission, isOwner } = useAuth();

  if (authState.isLoading) {
    return <div>Loading...</div>;
  }

  if (!authState.isAuthenticated) {
    return <Navigate to={redirectTo} replace />;
  }

  // Role-based check
  if (requiredRole && !hasPermission(requiredRole)) {
    return <Navigate to="/unauthorized" replace />;
  }

  // Ownership check (for user-specific resources)
  if (ownerOnly && ownerId !== undefined && !isOwner(ownerId)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <Outlet />;
};

export default ProtectedRoute;

5. Updated EmployeeService with Auth (src/services/employeeService.ts)



import axios from 'axios';
import { Employee } from '../models/Employee';
import { useAuth } from '../contexts/AuthContext';

const API_URL = 'http://localhost:9090/api/employees';

// Create axios instance with auth interceptor
const api = axios.create();

api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = token;
  }
  return config;
});

export const employeeService = {
  async getEmployees(): Promise<Employee[]> {
    const response = await api.get(API_URL);
    return response.data;
  },

  async addEmployee(employee: Omit<Employee, 'empId'>): Promise<Employee> {
    const response = await api.post(API_URL, employee);
    return response.data;
  },

  // Add other CRUD operations
};


6. Updated Route Configuration (src/App.tsx)


import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import Login from './pages/Login';
import EmployeeList from './pages/EmployeeList';
import EmployeeProfile from './pages/EmployeeProfile';
import AdminDashboard from './pages/AdminDashboard';
import Unauthorized from './pages/Unauthorized';

const App: React.FC = () => {
  return (
    <Router>
      <AuthProvider>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/unauthorized" element={<Unauthorized />} />

          {/* Employee routes */}
          <Route element={<ProtectedRoute requiredRole="EMPLOYEE" />}>
            <Route path="/dashboard" element={<div>Employee Dashboard</div>} />
            <Route path="/profile/:id" element={<EmployeeProfile />} />
          </Route>

          {/* Manager routes */}
          <Route element={<ProtectedRoute requiredRole="MANAGER" />}>
            <Route path="/employees" element={<EmployeeList />} />
          </Route>

          {/* Admin routes */}
          <Route element={<ProtectedRoute requiredRole="ADMIN" />}>
            <Route path="/admin" element={<AdminDashboard />} />
            <Route path="/admin/users" element={<div>User Management</div>} />
          </Route>

          {/* Owner-protected route example */}
          <Route element={<ProtectedRoute ownerOnly />}>
            <Route path="/my-profile" element={<EmployeeProfile />} />
          </Route>

          <Route path="/" element={<Navigate to="/dashboard" replace />} />
        </Routes>
      </AuthProvider>
    </Router>
  );
};

export default App;


7. Employee List with Role-Based Actions (src/pages/EmployeeList.tsx)



import React from 'react';
import { useAuth } from '../contexts/AuthContext';
import { Button } from 'react-bootstrap';

const EmployeeList: React.FC = () => {
  const { authState, hasPermission } = useAuth();

  return (
    <div>
      <h1>Employee List</h1>
      
      {/* Only show add button for managers and admins */}
      {(hasPermission('MANAGER') || hasPermission('ADMIN')) && (
        <Button variant="success" className="mb-3">
          Add Employee
        </Button>
      )}

      {/* Show export button only for admins */}
      {hasPermission('ADMIN') && (
        <Button variant="secondary" className="ms-2">
          Export Data
        </Button>
      )}

      {/* Table content */}
      <table className="table">
        {/* ... */}
        <tbody>
          {employees.map(emp => (
            <tr key={emp.empId}>
              <td>{emp.empName}</td>
              {/* ... other columns */}
              <td>
                {/* Edit button for managers+ */}
                {(hasPermission('MANAGER') || hasPermission('ADMIN')) && (
                  <Button variant="primary" size="sm">
                    Edit
                  </Button>
                )}
                
                {/* Delete button only for admins */}
                {hasPermission('ADMIN') && (
                  <Button variant="danger" size="sm" className="ms-2">
                    Delete
                  </Button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default EmployeeList;


Key Integration Points with Your Backend:


  1. Authentication Flow:

    • Matches your UserController endpoint (/users/{userName}&{password})

    • Expects an APIResponse<User> structure in return


  2. Authorization Header:

    • Uses the token in the Authorization header as your EmployeesController expects

    • Matches the @RequestHeader(value="authorization") parameter


  3. Role-Based Access:

    • Uses the role.roleName from your User entity

    • Supports your Role entity structure with roleId and roleName


  4. User Context:

    • Maintains the userId and isActive status from your User entity

Recommended Backend Modifications:


  1. JWT Implementation:

    • Your backend should return a proper JWT token on login

    • Add a /validate-token endpoint for frontend token validation


  2. Enhanced Role System:

    • Consider adding permissions/privileges to roles

    • Add endpoint to fetch all roles for user management


  3. Password Security:

    • Never return passwords in API responses

    • Ensure passwords are properly hashed server-side


This implementation provides a complete integration with your backend while maintaining strong TypeScript typing and React best practices. The system supports:

  1. User authentication against your existing endpoint

  2. Role-based access control using your Role entity

  3. Token-based authorization for API calls

  4. Ownership checks for user-specific resources

  5. Comprehensive type safety throughout the application

No comments:

Post a Comment