login flow added

This commit is contained in:
laxmanhalaki 2025-10-29 19:40:16 +05:30
parent 913a8d2c1b
commit 96d8bedf6d
15 changed files with 2867 additions and 112 deletions

969
.cursor/project_setup.md Normal file
View File

@ -0,0 +1,969 @@
# RE Workflow Management System - Frontend Setup Guide
**Version:** 1.0
**Date:** October 16, 2025
**Technology Stack:** React 19 + TypeScript 5.7 + Vite 6.0 + Redux Toolkit 2.5 + Material-UI 6.3
---
## Table of Contents
1. [Frontend Architecture Overview](#frontend-architecture-overview)
2. [Technology Stack](#technology-stack)
3. [Project Folder Structure](#project-folder-structure)
4. [TypeScript Configuration](#typescript-configuration)
5. [State Management (Redux Toolkit)](#state-management-redux-toolkit)
6. [Component Structure](#component-structure)
7. [Configuration Management](#configuration-management)
8. [Deployment Architecture](#deployment-architecture)
9. [Development Setup Instructions](#development-setup-instructions)
---
## 1. Frontend Architecture Overview
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ React.js SPA (Single Page Application) │
│ - Material-UI / Ant Design Components │
│ - Redux Toolkit for State Management │
│ - React Router for Navigation │
│ - Axios for API Communication │
│ - Vite for Build & Development │
└─────────────────────────────────────────────────────────────┘
↕ HTTPS/REST API
┌─────────────────────────────────────────────────────────────┐
│ BACKEND API LAYER │
│ Express.js REST API Server │
│ - Node.js + TypeScript │
│ - PostgreSQL Database │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. Technology Stack
| Component | Technology | Version | Purpose |
|-----------|-----------|---------|---------|
| **Frontend Library** | React.js | 19.0.x | UI component library |
| **Frontend Language** | TypeScript (TSX) | 5.7.x | Type-safe frontend development |
| **UI Framework** | Material-UI (MUI) | 6.3.x | Component design system |
| **Build Tool** | Vite | 6.0.x | Ultra-fast build & dev server |
| **State Management** | Redux Toolkit | 2.5.x | Application state management |
| **Routing** | React Router DOM | 7.1.x | Client-side routing |
| **HTTP Client** | Axios | 1.7.x | API communication |
| **Form Management** | Formik | 2.4.x | Form state management |
| **Form Validation** | Yup | 1.6.x | Schema validation |
| **Date Utilities** | date-fns | 4.1.x | Date manipulation |
| **Charts** | Recharts | 2.15.x | Data visualization |
| **Notifications** | React Toastify | 11.0.x | Toast notifications |
| **File Upload** | React Dropzone | 14.3.x | File upload UI |
| **Testing** | Vitest + React Testing Library | 2.1.x/16.1.x | Component testing |
| **Code Quality** | ESLint + Prettier | 9.x/3.x | Code linting & formatting |
---
## 3. Project Folder Structure
### Frontend Repository (`re-workflow-frontend`)
```
re-workflow-frontend/
├── public/ # Static assets
│ ├── index.html
│ ├── favicon.ico
│ ├── robots.txt
│ └── manifest.json
├── src/ # Source code
│ ├── index.tsx # Application entry point
│ ├── App.tsx # Root component
│ ├── App.css
│ ├── react-app-env.d.ts # React TypeScript declarations
│ │
│ ├── types/ # TypeScript type definitions
│ │ ├── index.ts # Main types export
│ │ ├── user.types.ts # User types
│ │ ├── workflow.types.ts # Workflow types
│ │ ├── approval.types.ts # Approval types
│ │ ├── document.types.ts # Document types
│ │ ├── notification.types.ts # Notification types
│ │ ├── common.types.ts # Common types
│ │ └── api.types.ts # API response types
│ │
│ ├── assets/ # Static assets
│ │ ├── images/
│ │ ├── icons/
│ │ ├── fonts/
│ │ └── styles/
│ │ ├── global.css
│ │ ├── variables.css
│ │ └── theme.ts
│ │
│ ├── components/ # Reusable components
│ │ ├── common/ # Generic components
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.module.css
│ │ │ │ ├── Button.types.ts
│ │ │ │ └── Button.test.tsx
│ │ │ ├── Input/
│ │ │ ├── Dropdown/
│ │ │ ├── Modal/
│ │ │ ├── Card/
│ │ │ ├── Table/
│ │ │ ├── Tabs/
│ │ │ ├── Pagination/
│ │ │ ├── Loader/
│ │ │ ├── Notification/
│ │ │ └── ErrorBoundary/
│ │ │
│ │ ├── layout/ # Layout components
│ │ │ ├── Header/
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Header.module.css
│ │ │ │ └── Header.types.ts
│ │ │ ├── Sidebar/
│ │ │ ├── Footer/
│ │ │ ├── Navigation/
│ │ │ └── PageLayout/
│ │ │
│ │ ├── workflow/ # Workflow-specific components
│ │ │ ├── TemplateSelector/
│ │ │ ├── BasicInformation/
│ │ │ ├── ApprovalWorkflow/
│ │ │ │ ├── ApprovalWorkflow.tsx
│ │ │ │ ├── ApproverLevel.tsx
│ │ │ │ ├── TATCalculator.tsx
│ │ │ │ ├── ApprovalSummary.tsx
│ │ │ │ └── types.ts
│ │ │ ├── ParticipantAccess/
│ │ │ │ ├── ParticipantAccess.tsx
│ │ │ │ ├── SpectatorList.tsx
│ │ │ │ ├── UserTagging.tsx
│ │ │ │ └── types.ts
│ │ │ ├── DocumentUpload/
│ │ │ │ ├── DocumentUpload.tsx
│ │ │ │ ├── FilePreview.tsx
│ │ │ │ ├── GoogleDocsLink.tsx
│ │ │ │ └── types.ts
│ │ │ ├── ReviewSubmit/
│ │ │ └── WizardStepper/
│ │ │
│ │ ├── request/ # Request management components
│ │ │ ├── RequestCard/
│ │ │ ├── RequestList/
│ │ │ ├── RequestDetail/
│ │ │ │ ├── RequestOverview.tsx
│ │ │ │ ├── WorkflowTab.tsx
│ │ │ │ ├── DocumentTab.tsx
│ │ │ │ ├── ActivityTab.tsx
│ │ │ │ ├── TATProgressBar.tsx
│ │ │ │ └── types.ts
│ │ │ ├── StatusBadge/
│ │ │ └── PriorityIndicator/
│ │ │
│ │ ├── approval/ # Approval action components
│ │ │ ├── ApprovalModal/
│ │ │ ├── RejectionModal/
│ │ │ ├── ApprovalHistory/
│ │ │ └── ConclusionRemark/
│ │ │
│ │ ├── workNote/ # Work notes / chat components
│ │ │ ├── WorkNoteChat/
│ │ │ ├── MessageItem/
│ │ │ ├── MessageComposer/
│ │ │ ├── FileAttachment/
│ │ │ └── UserMention/
│ │ │
│ │ ├── notification/ # Notification components
│ │ │ ├── NotificationBell/
│ │ │ ├── NotificationList/
│ │ │ ├── NotificationItem/
│ │ │ └── NotificationSettings/
│ │ │
│ │ └── dashboard/ # Dashboard components
│ │ ├── DashboardCard/
│ │ ├── StatisticsWidget/
│ │ ├── RecentRequests/
│ │ └── QuickActions/
│ │
│ ├── pages/ # Page components (routes)
│ │ ├── Auth/
│ │ │ ├── Login.tsx
│ │ │ └── SSOCallback.tsx
│ │ ├── Dashboard/
│ │ │ └── Dashboard.tsx
│ │ ├── CreateRequest/
│ │ │ └── CreateRequest.tsx
│ │ ├── MyRequests/
│ │ │ └── MyRequests.tsx
│ │ ├── OpenRequests/
│ │ │ └── OpenRequests.tsx
│ │ ├── ClosedRequests/
│ │ │ └── ClosedRequests.tsx
│ │ ├── RequestDetail/
│ │ │ └── RequestDetail.tsx
│ │ ├── NotFound/
│ │ │ └── NotFound.tsx
│ │ └── Unauthorized/
│ │ └── Unauthorized.tsx
│ │
│ ├── redux/ # Redux state management
│ │ ├── store.ts # Redux store configuration
│ │ ├── hooks.ts # Typed Redux hooks
│ │ ├── slices/
│ │ │ ├── authSlice.ts
│ │ │ ├── workflowSlice.ts
│ │ │ ├── approvalSlice.ts
│ │ │ ├── notificationSlice.ts
│ │ │ ├── documentSlice.ts
│ │ │ ├── workNoteSlice.ts
│ │ │ ├── participantSlice.ts
│ │ │ └── uiSlice.ts
│ │ └── middleware/
│ │ └── apiMiddleware.ts
│ │
│ ├── services/ # API service layer
│ │ ├── api.ts # Axios instance configuration
│ │ ├── auth.service.ts
│ │ ├── workflow.service.ts
│ │ ├── approval.service.ts
│ │ ├── document.service.ts
│ │ ├── notification.service.ts
│ │ ├── workNote.service.ts
│ │ ├── participant.service.ts
│ │ ├── dashboard.service.ts
│ │ └── user.service.ts
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useAuth.ts
│ │ ├── useWorkflow.ts
│ │ ├── useNotification.ts
│ │ ├── useDebounce.ts
│ │ ├── useInfiniteScroll.ts
│ │ ├── useLocalStorage.ts
│ │ └── useWebSocket.ts
│ │
│ ├── utils/ # Utility functions
│ │ ├── constants.ts
│ │ ├── validators.ts
│ │ ├── formatters.ts
│ │ ├── dateUtils.ts
│ │ ├── fileUtils.ts
│ │ ├── errorHandler.ts
│ │ └── helpers.ts
│ │
│ ├── routes/ # React Router configuration
│ │ ├── AppRoutes.tsx
│ │ ├── PrivateRoute.tsx
│ │ └── PublicRoute.tsx
│ │
│ └── config/ # Frontend configuration
│ ├── api.config.ts
│ ├── theme.config.ts
│ └── constants.config.ts
├── dist/ # Build output (Vite)
├── tests/ # Test files
│ ├── components/
│ ├── pages/
│ ├── redux/
│ ├── services/
│ └── setup.js
├── docs/ # Component documentation
│ └── storybook/
├── nginx/ # Nginx configuration for production
│ └── default.conf
├── .env.example
├── .env.development
├── .env.production
├── .eslintrc.json
├── .prettierrc
├── .gitignore
├── .dockerignore
├── Dockerfile
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── package.json
├── package-lock.json
└── README.md
```
---
## 4. TypeScript Configuration
### `tsconfig.json`
```json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"noEmit": true,
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@components/*": ["components/*"],
"@pages/*": ["pages/*"],
"@services/*": ["services/*"],
"@redux/*": ["redux/*"],
"@hooks/*": ["hooks/*"],
"@utils/*": ["utils/*"],
"@types/*": ["types/*"],
"@config/*": ["config/*"],
"@assets/*": ["assets/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "build", "dist"]
}
```
### `vite.config.ts`
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@pages': path.resolve(__dirname, './src/pages'),
'@services': path.resolve(__dirname, './src/services'),
'@redux': path.resolve(__dirname, './src/redux'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@utils': path.resolve(__dirname, './src/utils'),
'@types': path.resolve(__dirname, './src/types'),
'@config': path.resolve(__dirname, './src/config'),
'@assets': path.resolve(__dirname, './src/assets'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
});
```
---
## 5. State Management (Redux Toolkit)
### Redux Store Configuration
#### `src/redux/store.ts`
```typescript
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import workflowReducer from './slices/workflowSlice';
import approvalReducer from './slices/approvalSlice';
import notificationReducer from './slices/notificationSlice';
import documentReducer from './slices/documentSlice';
import workNoteReducer from './slices/workNoteSlice';
import participantReducer from './slices/participantSlice';
import uiReducer from './slices/uiSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
workflow: workflowReducer,
approval: approvalReducer,
notification: notificationReducer,
document: documentReducer,
workNote: workNoteReducer,
participant: participantReducer,
ui: uiReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'],
},
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
### Typed Redux Hooks
#### `src/redux/hooks.ts`
```typescript
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
### Type Definitions
#### `src/types/common.types.ts`
```typescript
export enum Priority {
STANDARD = 'STANDARD',
EXPRESS = 'EXPRESS'
}
export enum WorkflowStatus {
DRAFT = 'DRAFT',
PENDING = 'PENDING',
IN_PROGRESS = 'IN_PROGRESS',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
CLOSED = 'CLOSED'
}
export interface ApiResponse<T = any> {
success: boolean;
message: string;
data?: T;
error?: string;
timestamp: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
```
---
## 6. Component Structure
### Example: Button Component
#### `src/components/common/Button/Button.tsx`
```typescript
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
fullWidth?: boolean;
}
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false,
fullWidth = false,
}) => {
return (
<button
className={`${styles.button} ${styles[variant]} ${
fullWidth ? styles.fullWidth : ''
}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
export default Button;
```
#### `src/components/common/Button/Button.types.ts`
```typescript
export interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
fullWidth?: boolean;
}
```
---
## 7. Configuration Management
### Environment Variables
#### `.env.example`
```bash
# frontend/.env.example
# Application
REACT_APP_ENV=development
REACT_APP_NAME=RE Workflow Management
REACT_APP_VERSION=1.0.0
# API Configuration
REACT_APP_API_BASE_URL=http://localhost:5000/api/v1
REACT_APP_API_TIMEOUT=30000
# SSO Configuration
REACT_APP_SSO_LOGIN_URL=http://localhost:5000/api/v1/auth/login
REACT_APP_SSO_LOGOUT_URL=http://localhost:5000/api/v1/auth/logout
# Feature Flags
REACT_APP_ENABLE_NOTIFICATIONS=true
REACT_APP_ENABLE_EMAIL_NOTIFICATIONS=false
REACT_APP_ENABLE_ANALYTICS=true
# File Upload
REACT_APP_MAX_FILE_SIZE_MB=10
REACT_APP_ALLOWED_FILE_TYPES=pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif
# Google Integration
REACT_APP_GOOGLE_API_KEY=your_google_api_key
# UI Configuration
REACT_APP_THEME_PRIMARY_COLOR=#1976d2
REACT_APP_ITEMS_PER_PAGE=20
```
### API Configuration
#### `src/config/api.config.ts`
```typescript
export const API_CONFIG = {
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1',
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
headers: {
'Content-Type': 'application/json',
},
};
export const API_ENDPOINTS = {
auth: {
login: '/auth/login',
logout: '/auth/logout',
me: '/auth/me',
},
workflows: {
getAll: '/workflows',
create: '/workflows',
getById: (id: string) => `/workflows/${id}`,
update: (id: string) => `/workflows/${id}`,
submit: (id: string) => `/workflows/${id}/submit`,
},
// ... more endpoints
};
```
---
## 8. Deployment Architecture
### Dockerfile
#### `Dockerfile`
```dockerfile
# re-workflow-frontend/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build application with Vite
RUN npm run build
# =====================================
# Production Image with Nginx
# =====================================
FROM nginx:alpine
# Copy custom nginx config
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
# Copy built files from builder (Vite outputs to 'dist' by default)
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
```
### Nginx Configuration
#### `nginx/default.conf`
```nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# React Router support
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
```
### CI/CD Pipeline (GitHub Actions)
#### `.github/workflows/frontend-deploy.yml`
```yaml
# .github/workflows/frontend-deploy.yml
name: Frontend CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Build Docker image
run: docker build -t gcr.io/re-project/re-workflow-frontend:${{ github.sha }} .
- name: Push to GCR
run: docker push gcr.io/re-project/re-workflow-frontend:${{ github.sha }}
```
---
## 9. Development Setup Instructions
### 9.1 Prerequisites
- **Node.js:** v22.x LTS
- **npm:** v10.x or higher
- **Git:** Latest version
- **TypeScript:** v5.7.x (installed as dev dependency)
- **Backend API:** Running on `http://localhost:5000` (see backend documentation)
### 9.2 Local Development Setup
#### Step 1: Clone Frontend Repository
```bash
git clone https://github.com/royalenfield/re-workflow-frontend.git
cd re-workflow-frontend
```
#### Step 2: Configure Frontend Environment
```bash
# Copy environment file
cp .env.example .env
# Edit .env with backend API URL
nano .env # or use your preferred editor
# Set REACT_APP_API_BASE_URL=http://localhost:5000/api/v1
```
#### Step 3: Install Dependencies & Run Frontend
```bash
# Install dependencies
npm install
# Run TypeScript type checking
npm run type-check
# Start development server (Vite with hot reload)
npm run dev
# Frontend will run on: http://localhost:3000
```
#### Step 4: Access Application
- **Frontend:** http://localhost:3000
- **Backend API:** http://localhost:5000
- **API Documentation:** http://localhost:5000/api-docs
- **Health Check:** http://localhost:5000/health
### 9.3 Docker Setup
```bash
# From frontend repository root
cd re-workflow-frontend
# Copy environment file
cp .env.example .env
# Build Docker image
docker build -t re-workflow-frontend .
# Run frontend container
docker run -d -p 3000:80 --name frontend re-workflow-frontend
# Access frontend: http://localhost:3000
# Stop container
docker stop frontend
docker rm frontend
```
### 9.4 Running Tests
```bash
# From frontend repository
cd re-workflow-frontend
npm test # Run all tests (Vitest)
npm run test:ui # Interactive UI mode (Vitest)
npm run test:coverage # With coverage report
# Coverage report will be in: coverage/
```
### 9.5 Code Quality Checks
```bash
# From frontend repository
cd re-workflow-frontend
npm run lint # ESLint check (React + TypeScript rules)
npm run lint:fix # Auto-fix issues
npm run format # Prettier formatting
npm run type-check # TypeScript type checking only (no build)
# Run all quality checks together
npm run lint && npm run type-check && npm test
```
### 9.6 Git Workflow
```bash
# Feature branch workflow
git checkout -b feature/your-feature-name
git add .
git commit -m "feat: add your feature description"
git push origin feature/your-feature-name
# Create Pull Request on GitHub/GitLab
# After approval and merge:
git checkout main
git pull origin main
```
**Branch Strategy:**
- `main` - Production-ready code
- `develop` - Integration branch for features
- `feature/*` - New features
- `bugfix/*` - Bug fixes
- `hotfix/*` - Production hotfixes
**Commit Message Convention (Conventional Commits):**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `style:` - Code style changes (formatting)
- `refactor:` - Code refactoring
- `test:` - Test additions or changes
- `chore:` - Build process or tool changes
---
## 10. Package.json
```json
{
"name": "re-workflow-frontend",
"version": "1.0.0",
"description": "Royal Enfield Workflow Management System - Frontend (TypeScript)",
"private": true,
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"@reduxjs/toolkit": "^2.5.0",
"react-redux": "^9.2.0",
"@mui/material": "^6.3.0",
"@mui/icons-material": "^6.3.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"axios": "^1.7.9",
"formik": "^2.4.6",
"yup": "^1.6.3",
"zod": "^3.24.1",
"react-dropzone": "^14.3.5",
"date-fns": "^4.1.0",
"recharts": "^2.15.0",
"react-toastify": "^11.0.2"
},
"devDependencies": {
"@types/react": "^19.0.6",
"@types/react-dom": "^19.0.2",
"@types/node": "^22.10.5",
"typescript": "^5.7.2",
"vite": "^6.0.7",
"@vitejs/plugin-react": "^4.3.4",
"@testing-library/react": "^16.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.5.2",
"eslint": "^9.17.0",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"prettier": "^3.4.2",
"vitest": "^2.1.8"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"type-check": "tsc --noEmit"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
},
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0"
}
}
```
---
## Summary
This frontend documentation provides:
**Complete Frontend Architecture** - React SPA with Vite, Redux Toolkit, TypeScript
**Technology Stack** - React 19 + TypeScript 5.7 + Vite 6.0 + Material-UI 6.3
**Folder Structure** - Detailed organization with TypeScript conventions
**Type Definitions** - Comprehensive type safety with interfaces and enums
**Redux Toolkit Setup** - Fully typed state management
**Component Structure** - Reusable, testable components
**Configuration Management** - Environment variables and settings
**Deployment Architecture** - Docker, Nginx, CI/CD pipelines
**Development Setup** - Step-by-step installation and configuration
**Testing Strategy** - Vitest and React Testing Library
**Code Quality** - ESLint, Prettier, TypeScript best practices
**Technology Stack:** React 19 + TypeScript 5.7 + Vite 6.0 + Redux Toolkit 2.5 + Material-UI 6.3
**Repository:** `re-workflow-frontend` (Independent Repository)
**Status:** ✅ Ready for Implementation

View File

@ -9,6 +9,8 @@ import { WorkNoteChat } from '@/components/workNote/WorkNoteChat';
import { CreateRequest } from '@/pages/CreateRequest'; import { CreateRequest } from '@/pages/CreateRequest';
import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard'; import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
import { MyRequests } from '@/pages/MyRequests'; import { MyRequests } from '@/pages/MyRequests';
import { Profile } from '@/pages/Profile';
import { Settings } from '@/pages/Settings';
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal'; import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -55,6 +57,15 @@ function AppRoutes({ onLogout }: AppProps) {
}, [dynamicRequests]); }, [dynamicRequests]);
const handleNavigate = (page: string) => { const handleNavigate = (page: string) => {
// Handle special routes
if (page === 'profile') {
navigate('/profile');
return;
}
if (page === 'settings') {
navigate('/settings');
return;
}
navigate(`/${page}`); navigate(`/${page}`);
}; };
@ -556,6 +567,26 @@ function AppRoutes({ onLogout }: AppProps) {
/> />
} }
/> />
{/* Profile */}
<Route
path="/profile"
element={
<PageLayout currentPage="profile" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<Profile />
</PageLayout>
}
/>
{/* Settings */}
<Route
path="/settings"
element={
<PageLayout currentPage="settings" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<Settings />
</PageLayout>
}
/>
</Routes> </Routes>
<Toaster <Toaster
@ -585,10 +616,18 @@ function AppRoutes({ onLogout }: AppProps) {
} }
// Main App Component with Router // Main App Component with Router
export default function App() { interface MainAppProps {
onLogout?: () => void;
}
export default function App(props?: MainAppProps) {
const { onLogout } = props || {};
console.log('🟢 Main App component rendered');
console.log('🟢 onLogout prop received:', !!onLogout);
return ( return (
<BrowserRouter> <BrowserRouter>
<AppRoutes /> <AppRoutes onLogout={onLogout} />
</BrowserRouter> </BrowserRouter>
); );
} }

View File

@ -5,6 +5,17 @@ import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useAuth } from '@/contexts/AuthContext';
import royalEnfieldLogo from '@/assets/images/royal_enfield_logo.png'; import royalEnfieldLogo from '@/assets/images/royal_enfield_logo.png';
interface PageLayoutProps { interface PageLayoutProps {
@ -17,6 +28,23 @@ interface PageLayoutProps {
export function PageLayout({ children, currentPage = 'dashboard', onNavigate, onNewRequest, onLogout }: PageLayoutProps) { export function PageLayout({ children, currentPage = 'dashboard', onNavigate, onNewRequest, onLogout }: PageLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const { user } = useAuth();
// Get user initials for avatar
const getUserInitials = () => {
if (user?.displayName) {
const names = user.displayName.split(' ').filter(Boolean);
if (names.length >= 2) {
return `${names[0]?.[0] || ''}${names[names.length - 1]?.[0] || ''}`.toUpperCase();
}
return user.displayName.substring(0, 2).toUpperCase();
}
if (user?.email) {
return user.email.substring(0, 2).toUpperCase();
}
return 'U';
};
const menuItems = [ const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: Home }, { id: 'dashboard', label: 'Dashboard', icon: Home },
@ -188,20 +216,32 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Avatar className="cursor-pointer shrink-0 h-10 w-10"> <Avatar className="cursor-pointer shrink-0 h-10 w-10">
<AvatarImage src="" /> <AvatarImage src={user?.picture || ''} />
<AvatarFallback className="bg-re-green text-white text-sm">JD</AvatarFallback> <AvatarFallback className="bg-re-green text-white text-sm">
{getUserInitials()}
</AvatarFallback>
</Avatar> </Avatar>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem> <DropdownMenuItem onClick={() => onNavigate?.('profile')}>
<User className="w-4 h-4 mr-2" /> <User className="w-4 h-4 mr-2" />
Profile Profile
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem
onClick={(e) => {
e.preventDefault();
// Settings page not ready yet - do nothing for now
}}
className="opacity-50 cursor-not-allowed"
>
<Settings className="w-4 h-4 mr-2" /> <Settings className="w-4 h-4 mr-2" />
Settings Settings
<span className="ml-auto text-xs text-gray-400">Coming soon</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={onLogout} className="text-red-600 focus:text-red-600"> <DropdownMenuItem
onClick={() => setShowLogoutDialog(true)}
className="text-red-600 focus:text-red-600"
>
<LogOut className="w-4 h-4 mr-2" /> <LogOut className="w-4 h-4 mr-2" />
Logout Logout
</DropdownMenuItem> </DropdownMenuItem>
@ -215,6 +255,48 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
{children} {children}
</main> </main>
</div> </div>
{/* Logout Confirmation Dialog */}
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<LogOut className="w-5 h-5 text-red-600" />
Confirm Logout
</AlertDialogTitle>
<AlertDialogDescription className="pt-2">
Are you sure you want to logout? You will need to sign in again to access your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setShowLogoutDialog(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
console.log('🔴 Logout button clicked in PageLayout');
console.log('🔴 onLogout function exists?', !!onLogout);
setShowLogoutDialog(false);
if (onLogout) {
console.log('🔴 Calling onLogout function...');
try {
await onLogout();
console.log('🔴 onLogout completed');
} catch (error) {
console.error('🔴 Error calling onLogout:', error);
}
} else {
console.error('🔴 ERROR: onLogout is undefined!');
}
}}
className="bg-red-600 hover:bg-red-700 text-white focus:ring-red-600"
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
} }

View File

@ -28,40 +28,40 @@ function AlertDialogPortal({
); );
} }
function AlertDialogOverlay({ const AlertDialogOverlay = React.forwardRef<
className, React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
...props React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { >(({ className, ...props }, ref) => (
return ( <AlertDialogPrimitive.Overlay
<AlertDialogPrimitive.Overlay data-slot="alert-dialog-overlay"
data-slot="alert-dialog-overlay" className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
ref={ref}
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className, className,
)} )}
{...props} {...props}
/> />
); </AlertDialogPortal>
} ));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({ function AlertDialogHeader({
className, className,

View File

@ -0,0 +1,618 @@
/**
* Authentication Context
* Provides unified authentication interface that works with both:
* - Backend token exchange (localhost/development)
* - Auth0 Provider (production)
*/
import { createContext, useContext, useEffect, useState, ReactNode, useRef } from 'react';
import { Auth0Provider, useAuth0 as useAuth0Hook } from '@auth0/auth0-react';
import { TokenManager, isTokenExpired } from '../utils/tokenManager';
import { exchangeCodeForTokens, refreshAccessToken, getCurrentUser, logout as logoutApi } from '../services/authApi';
interface User {
userId?: string;
employeeId?: string;
email?: string;
firstName?: string;
lastName?: string;
displayName?: string;
department?: string;
designation?: string;
isAdmin?: boolean;
sub?: string;
name?: string;
picture?: string;
nickname?: string;
}
interface AuthContextType {
isAuthenticated: boolean;
isLoading: boolean;
user: User | null;
error: Error | null;
login: () => Promise<void>;
logout: () => Promise<void>;
getAccessTokenSilently: () => Promise<string | null>;
refreshTokenSilently: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
/**
* Check if running on localhost
*/
const isLocalhost = (): boolean => {
return (
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname === ''
);
};
/**
* Backend-based Auth Provider (for localhost)
*/
function BackendAuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoggingOut, setIsLoggingOut] = useState(false);
// Check authentication status on mount
// Only check if we're not in the middle of a logout process
useEffect(() => {
// PRIORITY 1: Check for logout flags in sessionStorage (survives page reload during logout)
const logoutFlag = sessionStorage.getItem('__logout_in_progress__');
const forceLogout = sessionStorage.getItem('__force_logout__');
if (logoutFlag === 'true' || forceLogout === 'true') {
console.log('🔴 Logout flag detected - PREVENTING auto-authentication');
console.log('🔴 Clearing ALL authentication data and showing login screen');
// Remove flags
sessionStorage.removeItem('__logout_in_progress__');
sessionStorage.removeItem('__force_logout__');
// Clear all tokens one more time (aggressive)
TokenManager.clearAll();
// Also manually clear everything
try {
localStorage.clear();
sessionStorage.clear();
} catch (e) {
console.error('Error clearing storage:', e);
}
// Set unauthenticated state
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
setError(null);
console.log('🔴 Logout complete - user should see login screen');
return;
}
// PRIORITY 2: Check if URL has logout parameter (from redirect)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('logout')) {
console.log('🔴 Logout parameter in URL - clearing everything');
TokenManager.clearAll();
localStorage.clear();
sessionStorage.clear();
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
// Clean URL
window.history.replaceState({}, document.title, '/');
return;
}
// PRIORITY 3: Check if this is a logout redirect by checking if all tokens are cleared
const token = TokenManager.getAccessToken();
const refreshToken = TokenManager.getRefreshToken();
const userData = TokenManager.getUserData();
const hasAuthData = token || refreshToken || userData;
// If no auth data exists, we're likely after a logout - set unauthenticated state immediately
if (!hasAuthData) {
console.log('🔴 No auth data found - setting unauthenticated state');
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
return;
}
// PRIORITY 4: Only check auth status if we have some auth data AND we're not logging out
if (!isLoggingOut) {
checkAuthStatus();
} else {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false);
}
}, [isLoggingOut]);
// Silent refresh interval
useEffect(() => {
if (!isAuthenticated) return;
const checkAndRefresh = async () => {
const token = TokenManager.getAccessToken();
if (token && isTokenExpired(token, 5)) {
// Token expires in less than 5 minutes, refresh it
try {
await refreshTokenSilently();
} catch (error) {
console.error('Silent refresh failed:', error);
}
}
};
// Check every 5 minutes
const interval = setInterval(checkAndRefresh, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [isAuthenticated]);
// Handle callback from OAuth redirect
// Use ref to prevent duplicate calls (React StrictMode runs effects twice in dev)
const callbackProcessedRef = useRef(false);
useEffect(() => {
// Skip if already processed or not on callback page
if (callbackProcessedRef.current || window.location.pathname !== '/login/callback') {
return;
}
const handleCallback = async () => {
// Mark as processed immediately to prevent duplicate calls
callbackProcessedRef.current = true;
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const errorParam = urlParams.get('error');
// Clean URL immediately to prevent re-running on re-renders
window.history.replaceState({}, document.title, '/login/callback');
if (errorParam) {
setError(new Error(`Authentication error: ${errorParam}`));
setIsLoading(false);
return;
}
if (!code) {
setIsLoading(false);
return;
}
try {
setIsLoading(true);
setIsAuthenticated(false);
setError(null);
// IMPORTANT: redirectUri must match the one used in initial Okta authorization request
// This is the frontend callback URL, NOT the backend URL
// Backend will use this same URI when exchanging code with Okta
const redirectUri = `${window.location.origin}/login/callback`;
console.log('📥 Authorization Code Received:', {
code: code.substring(0, 10) + '...',
redirectUri,
fullUrl: window.location.href,
note: 'redirectUri is frontend URL (not backend) - must match Okta registration',
});
const result = await exchangeCodeForTokens(code, redirectUri);
setUser(result.user);
setIsAuthenticated(true);
setError(null);
// Clean URL after success
window.history.replaceState({}, document.title, '/');
} catch (err: any) {
console.error('❌ Token exchange error in AuthContext:', err);
setError(err);
setIsAuthenticated(false);
setUser(null);
// Reset ref on error so user can retry if needed
callbackProcessedRef.current = false;
} finally {
setIsLoading(false);
}
};
handleCallback();
}, []); // Empty deps - only run once on mount
const checkAuthStatus = async () => {
// Don't check auth status if we're in the middle of logging out
if (isLoggingOut) {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false);
return;
}
try {
setIsLoading(true);
const token = TokenManager.getAccessToken();
const storedUser = TokenManager.getUserData();
console.log('🔍 Checking auth status:', { hasToken: !!token, hasUser: !!storedUser, isLoggingOut });
// If no token at all, user is not authenticated
if (!token) {
console.log('🔍 No token found - setting unauthenticated');
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
return;
}
// Check if token is expired
if (isTokenExpired(token)) {
// Token expired, try to refresh
try {
await refreshTokenSilently();
// Refresh succeeded, check again
const newToken = TokenManager.getAccessToken();
if (newToken && !isTokenExpired(newToken)) {
const refreshedUser = TokenManager.getUserData();
if (refreshedUser) {
setUser(refreshedUser);
setIsAuthenticated(true);
} else {
// Fetch user data
try {
const userData = await getCurrentUser();
setUser(userData);
TokenManager.setUserData(userData);
setIsAuthenticated(true);
} catch {
// Token might be invalid, clear everything
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
}
}
} else {
// Refresh failed, user is logged out
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
}
} catch {
// Refresh failed, clear everything
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
}
} else {
// Token is valid
if (storedUser) {
setUser(storedUser);
setIsAuthenticated(true);
} else {
// Fetch user data to ensure token is still valid
try {
const userData = await getCurrentUser();
setUser(userData);
TokenManager.setUserData(userData);
setIsAuthenticated(true);
} catch {
// Token might be invalid, clear everything
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
}
}
}
} catch (err: any) {
console.error('Error checking auth status:', err);
setError(err);
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
} finally {
setIsLoading(false);
}
};
const login = async () => {
try {
setError(null);
// Redirect to Okta login
const oktaDomain = 'https://dev-830839.oktapreview.com';
const clientId = '0oa2j8slwj5S4bG5k0h8';
const redirectUri = `${window.location.origin}/login/callback`;
const responseType = 'code';
const scope = 'openid profile email';
const state = Math.random().toString(36).substring(7);
const authUrl = `${oktaDomain}/oauth2/default/v1/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`response_type=${responseType}&` +
`scope=${encodeURIComponent(scope)}&` +
`state=${state}`;
window.location.href = authUrl;
} catch (err: any) {
setError(err);
throw err;
}
};
const logout = async () => {
console.log('🚪 LOGOUT FUNCTION CALLED - Starting logout process');
console.log('🚪 Current auth state:', { isAuthenticated, hasUser: !!user, isLoading });
try {
// Set logout flag to prevent auto-authentication after redirect
sessionStorage.setItem('__logout_in_progress__', 'true');
setIsLoggingOut(true);
console.log('🚪 Step 1: Resetting auth state...');
// Reset auth state FIRST to prevent any re-authentication
setIsAuthenticated(false);
setUser(null);
setError(null);
setIsLoading(true); // Set loading to prevent checkAuthStatus from running
console.log('🚪 Step 1: Auth state reset complete');
// Call backend logout API to clear server-side session and httpOnly cookies
// IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies
try {
console.log('🚪 Step 2: Calling backend logout API to clear httpOnly cookies...');
await logoutApi();
console.log('🚪 Step 2: Backend logout API completed - httpOnly cookies should be cleared');
} catch (err) {
console.error('🚪 Logout API error:', err);
console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared');
// Continue with logout even if API call fails
}
// Clear all authentication data
console.log('========================================');
console.log('LOGOUT - Clearing all authentication data');
console.log('========================================');
// Use TokenManager.clearAll() which does comprehensive cleanup
TokenManager.clearAll();
console.log('tryng to clrear all(localStorage and sessionStorage)')
// Double-check: Clear everything one more time as backup
try {
// Get all localStorage keys and remove them
const localStorageKeys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) localStorageKeys.push(key);
}
localStorageKeys.forEach(key => {
try {
localStorage.removeItem(key);
} catch (e) {
console.warn(`Failed to remove localStorage key ${key}:`, e);
}
});
localStorage.clear();
// Get all sessionStorage keys and remove them
const sessionStorageKeys: string[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) sessionStorageKeys.push(key);
}
sessionStorageKeys.forEach(key => {
try {
sessionStorage.removeItem(key);
} catch (e) {
console.warn(`Failed to remove sessionStorage key ${key}:`, e);
}
});
sessionStorage.clear();
console.log('Final verification - Storage cleared:');
console.log(`localStorage.length: ${localStorage.length}`);
console.log(`sessionStorage.length: ${sessionStorage.length}`);
if (localStorage.length > 0) {
console.error('ERROR: localStorage still has items:', Object.keys(localStorage));
}
if (sessionStorage.length > 0) {
console.error('ERROR: sessionStorage still has items:', Object.keys(sessionStorage));
}
} catch (e) {
console.error('Error in final cleanup:', e);
}
// Final verification BEFORE redirect
console.log('All authentication data cleared. Final verification:');
console.log(`localStorage.length: ${localStorage.length}`);
console.log(`sessionStorage.length: ${sessionStorage.length}`);
if (localStorage.length > 0) {
console.error('CRITICAL: localStorage still has items before redirect!', Object.keys(localStorage));
// Force clear one more time
const remainingKeys = Object.keys(localStorage);
remainingKeys.forEach(key => localStorage.removeItem(key));
localStorage.clear();
}
if (sessionStorage.length > 0) {
console.error('CRITICAL: sessionStorage still has items before redirect!', Object.keys(sessionStorage));
const remainingKeys = Object.keys(sessionStorage);
remainingKeys.forEach(key => sessionStorage.removeItem(key));
sessionStorage.clear();
}
console.log('Redirecting to login...');
// Ensure logout flag is set before redirect
sessionStorage.setItem('__logout_in_progress__', 'true');
// Small delay to ensure sessionStorage is written before redirect
await new Promise(resolve => setTimeout(resolve, 50));
// Use replace instead of href to prevent browser history issues
// DON'T use setTimeout - redirect immediately after clearing
if (isLocalhost()) {
// Direct redirect to login page for localhost - force full page reload
// Add timestamp to force fresh page load
window.location.replace('/?logout=' + Date.now());
} else {
// For production, redirect to Okta logout then to login
const oktaDomain = 'https://dev-830839.oktapreview.com';
const loginUrl = `${window.location.origin}/?logout=${Date.now()}`;
const logoutUrl = `${oktaDomain}/oauth2/default/v1/logout?post_logout_redirect_uri=${encodeURIComponent(loginUrl)}`;
window.location.replace(logoutUrl);
}
} catch (error) {
console.error('Logout error:', error);
// Force redirect even on error
localStorage.clear();
sessionStorage.clear();
window.location.replace('/');
}
};
const getAccessTokenSilently = async (): Promise<string | null> => {
const token = TokenManager.getAccessToken();
if (token && !isTokenExpired(token)) {
return token;
}
// Try to refresh
try {
await refreshTokenSilently();
return TokenManager.getAccessToken();
} catch {
return null;
}
};
const refreshTokenSilently = async (): Promise<void> => {
try {
const newToken = await refreshAccessToken();
if (newToken) {
// Token refreshed successfully
return;
}
throw new Error('Failed to refresh token');
} catch (err) {
TokenManager.clearAll();
setIsAuthenticated(false);
setUser(null);
throw err;
}
};
const value: AuthContextType = {
isAuthenticated,
isLoading,
user,
error,
login,
logout,
getAccessTokenSilently,
refreshTokenSilently,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Auth0-based Auth Provider (for production)
*/
function Auth0AuthProvider({ children }: { children: ReactNode }) {
return (
<Auth0Provider
domain="https://dev-830839.oktapreview.com/oauth2/default/v1"
clientId="0oa2j8slwj5S4bG5k0h8"
authorizationParams={{
redirect_uri: window.location.origin + '/login/callback',
}}
onRedirectCallback={(appState) => {
console.log('Auth0 Redirect Callback:', {
appState,
returnTo: appState?.returnTo || window.location.pathname,
});
}}
>
<Auth0ContextWrapper>{children}</Auth0ContextWrapper>
</Auth0Provider>
);
}
/**
* Wrapper to convert Auth0 hook to our context format
*/
function Auth0ContextWrapper({ children }: { children: ReactNode }) {
const {
isAuthenticated: auth0IsAuthenticated,
isLoading: auth0IsLoading,
user: auth0User,
error: auth0Error,
loginWithRedirect,
logout: logoutAuth0,
getAccessTokenSilently: getAuth0Token,
} = useAuth0Hook();
const getAccessTokenSilently = async (): Promise<string | null> => {
try {
return await getAuth0Token();
} catch {
return null;
}
};
const refreshTokenSilently = async (): Promise<void> => {
// Auth0 handles refresh automatically
try {
await getAuth0Token();
} catch {
// Silently fail, Auth0 will handle it
}
};
const value: AuthContextType = {
isAuthenticated: auth0IsAuthenticated,
isLoading: auth0IsLoading,
user: auth0User as User | null,
error: auth0Error as Error | null,
login: loginWithRedirect,
logout: logoutAuth0,
getAccessTokenSilently,
refreshTokenSilently,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Main Auth Provider - conditionally uses backend or Auth0
*/
export function AuthProvider({ children }: AuthProviderProps) {
if (isLocalhost()) {
return <BackendAuthProvider>{children}</BackendAuthProvider>;
}
return <Auth0AuthProvider>{children}</Auth0AuthProvider>;
}
/**
* Hook to use auth context
*/
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@ -1,34 +1,21 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { Auth0Provider } from '@auth0/auth0-react'; import { AuthProvider } from './contexts/AuthContext';
import { AuthenticatedApp } from './pages/Auth'; import { AuthenticatedApp } from './pages/Auth';
import './styles/globals.css'; import './styles/globals.css';
console.log('Application Starting...'); console.log('Application Starting...');
console.log('Auth0 Configuration:', { console.log('Environment:', {
domain: 'dev-dq1hiere8khcbdra.us.auth0.com', hostname: window.location.hostname,
clientId: '5gnC2Go5yRg3RXLuaFLr8KRc1fJ9PDJW', origin: window.location.origin,
origin: window.location.origin isLocalhost: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1',
}); });
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<Auth0Provider <AuthProvider>
domain="dev-dq1hiere8khcbdra.us.auth0.com"
clientId="5gnC2Go5yRg3RXLuaFLr8KRc1fJ9PDJW"
authorizationParams={{
redirect_uri: 'https://9b89f4bfd360.ngrok-free.app'
}}
onRedirectCallback={(appState) => {
console.log('Auth0 Redirect Callback:', {
appState,
returnTo: appState?.returnTo || window.location.pathname,
timestamp: new Date().toISOString()
});
}}
>
<AuthenticatedApp /> <AuthenticatedApp />
</Auth0Provider> </AuthProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -1,10 +1,10 @@
import { useAuth0 } from '@auth0/auth0-react'; import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { LogIn, Shield } from 'lucide-react'; import { LogIn, Shield } from 'lucide-react';
export function Auth() { export function Auth() {
const { loginWithRedirect, isLoading, error } = useAuth0(); const { login, isLoading, error } = useAuth();
console.log('Auth Component Render - Auth0 State:', { console.log('Auth Component Render - Auth0 State:', {
isLoading, isLoading,
@ -25,26 +25,8 @@ export function Auth() {
sessionStorage.clear(); sessionStorage.clear();
console.log('Storage cleared'); console.log('Storage cleared');
console.log('Calling loginWithRedirect with params:', {
authorizationParams: {
screen_hint: 'login',
prompt: 'login',
mode: 'login'
}
});
try { try {
await loginWithRedirect({ await login();
authorizationParams: {
screen_hint: 'login',
prompt: 'login',
mode: 'login'
},
appState: {
returnTo: window.location.pathname
}
});
console.log('Login redirect initiated successfully'); console.log('Login redirect initiated successfully');
} catch (loginError) { } catch (loginError) {
console.error('========================================'); console.error('========================================');

View File

@ -1,13 +1,15 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react'; import { useAuth } from '@/contexts/AuthContext';
import { Loader } from '@/components/common/Loader'; import { Loader } from '@/components/common/Loader';
import { useNavigate } from 'react-router-dom';
export function AuthCallback() { export function AuthCallback() {
const { isAuthenticated, isLoading, error, user } = useAuth0(); const { isAuthenticated, isLoading, error, user } = useAuth();
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
console.log('AuthCallback Component Mounted'); console.log('AuthCallback Component Mounted');
console.log('Auth0 State during callback:', { console.log('Auth State during callback:', {
isAuthenticated, isAuthenticated,
isLoading, isLoading,
error: error?.message, error: error?.message,
@ -17,19 +19,20 @@ export function AuthCallback() {
if (user) { if (user) {
console.log('User authenticated successfully:', { console.log('User authenticated successfully:', {
id: user.sub, userId: user.userId,
employeeId: user.employeeId,
email: user.email, email: user.email,
name: user.name, displayName: user.displayName,
picture: user.picture,
nickname: user.nickname,
userData: user userData: user
}); });
// Redirect to home after successful authentication
navigate('/');
} }
if (error) { if (error) {
console.error('Error during authentication callback:', error); console.error('Error during authentication callback:', error);
} }
}, [isAuthenticated, isLoading, error, user]); }, [isAuthenticated, isLoading, error, user, navigate]);
if (error) { if (error) {
console.error('AuthCallback Error:', error); console.error('AuthCallback Error:', error);

View File

@ -1,37 +1,42 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react'; import { useAuth } from '@/contexts/AuthContext';
import { Auth } from './Auth'; import { Auth } from './Auth';
import { AuthCallback } from './AuthCallback'; import { AuthCallback } from './AuthCallback';
import { AuthDebugInfo } from '@/components/Auth/AuthDebugInfo'; import { AuthDebugInfo } from '@/components/Auth/AuthDebugInfo';
import App from '../../App'; import App from '../../App';
export function AuthenticatedApp() { export function AuthenticatedApp() {
const { isAuthenticated, isLoading, error, user, logout: logoutAuth0 } = useAuth0(); const { isAuthenticated, isLoading, error, user, logout } = useAuth();
const [showDebugInfo, setShowDebugInfo] = useState(false); const [showDebugInfo, setShowDebugInfo] = useState(false);
const handleLogout = async () => { const handleLogout = async () => {
console.log('========================================'); console.log('🔵 ========================================');
console.log('LOGOUT - Initiated'); console.log('🔵 AuthenticatedApp.handleLogout - CALLED');
console.log('Timestamp:', new Date().toISOString()); console.log('🔵 Timestamp:', new Date().toISOString());
console.log('========================================'); console.log('🔵 logout function exists?', !!logout);
console.log('🔵 ========================================');
// Clear all storage
console.log('Clearing all storage...');
localStorage.clear();
sessionStorage.clear();
console.log('Storage cleared');
try { try {
await logoutAuth0({ if (!logout) {
logoutParams: { console.error('🔵 ERROR: logout function is undefined!');
returnTo: window.location.origin return;
}, }
localOnly: false // Clear Auth0 session as well
});
console.log('Logout successful'); console.log('🔵 Calling logout from auth context...');
// Call logout from auth context (handles all cleanup and redirect)
await logout();
console.log('🔵 Logout successful - redirecting to login...');
} catch (logoutError) { } catch (logoutError) {
console.error('Logout error:', logoutError); console.error('🔵 Logout error in handleLogout:', logoutError);
// Even if logout fails, clear local data and redirect
try {
console.log('🔵 Attempting emergency cleanup...');
localStorage.clear();
sessionStorage.clear();
window.location.href = '/';
} catch (e) {
console.error('🔵 Error during emergency cleanup:', e);
}
} }
}; };
@ -48,15 +53,18 @@ export function AuthenticatedApp() {
console.log('========================================'); console.log('========================================');
console.log('USER AUTHENTICATED - Full Details'); console.log('USER AUTHENTICATED - Full Details');
console.log('========================================'); console.log('========================================');
console.log('User ID (sub):', user.sub); console.log('User ID:', user.userId || user.sub);
console.log('Employee ID:', user.employeeId);
console.log('Email:', user.email); console.log('Email:', user.email);
console.log('Name:', user.name); console.log('Name:', user.displayName || user.name);
console.log('Nickname:', user.nickname); console.log('Display Name:', user.displayName);
console.log('Picture:', user.picture); console.log('First Name:', user.firstName);
console.log('Email Verified:', user.email_verified); console.log('Last Name:', user.lastName);
console.log('Updated At:', user.updated_at); console.log('Department:', user.department);
console.log('Designation:', user.designation);
console.log('Is Admin:', user.isAdmin);
console.log('========================================'); console.log('========================================');
console.log('ALL USER CLAIMS:'); console.log('ALL USER DATA:');
console.log(JSON.stringify(user, null, 2)); console.log(JSON.stringify(user, null, 2));
console.log('========================================'); console.log('========================================');
} }

View File

@ -0,0 +1,281 @@
import { useAuth } from '@/contexts/AuthContext';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
User,
Mail,
Briefcase,
Building2,
Phone,
Shield,
Calendar,
Edit,
CheckCircle
} from 'lucide-react';
export function Profile() {
const { user } = useAuth();
// Helper to get user initials for avatar
const getUserInitials = () => {
if (user?.displayName) {
const names = user.displayName.split(' ').filter(Boolean);
if (names.length >= 2) {
return `${names[0]?.[0] || ''}${names[names.length - 1]?.[0] || ''}`.toUpperCase();
}
return user.displayName.substring(0, 2).toUpperCase();
}
if (user?.email) {
return user.email.substring(0, 2).toUpperCase();
}
return 'U';
};
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header Card */}
<Card className="relative overflow-hidden shadow-xl border-0">
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"></div>
<CardContent className="relative z-10 p-8 lg:p-12">
<div className="flex flex-col md:flex-row items-start md:items-center gap-6">
<div className="relative">
<Avatar className="h-24 w-24 ring-4 ring-white/20 shadow-xl">
<AvatarImage src={user?.picture || ''} />
<AvatarFallback className="bg-yellow-400 text-slate-900 text-2xl font-bold">
{getUserInitials()}
</AvatarFallback>
</Avatar>
{user?.isAdmin && (
<div className="absolute -bottom-2 -right-2 bg-yellow-400 rounded-full p-1.5 shadow-lg">
<Shield className="w-4 h-4 text-slate-900" />
</div>
)}
</div>
<div className="flex-1 text-white">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold mb-2">
{user?.displayName || user?.name || 'User Profile'}
</h1>
<p className="text-lg text-gray-200 mb-3">
{user?.email || 'No email provided'}
</p>
<div className="flex flex-wrap gap-2">
{user?.isAdmin && (
<Badge className="bg-yellow-400 text-slate-900 hover:bg-yellow-400 font-semibold">
<Shield className="w-3 h-3 mr-1" />
Administrator
</Badge>
)}
{user?.employeeId && (
<Badge variant="outline" className="border-white/30 text-white bg-white/10">
ID: {user.employeeId}
</Badge>
)}
</div>
</div>
<Button
variant="secondary"
className="bg-white text-slate-900 hover:bg-gray-100"
>
<Edit className="w-4 h-4 mr-2" />
Edit Profile
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Profile Information Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Personal Information */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-100 rounded-lg">
<User className="h-5 w-5 text-blue-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Personal Information</CardTitle>
<CardDescription className="text-gray-600">Your personal details</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<Mail className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Email</p>
<p className="text-base text-gray-900 break-words">
{user?.email || 'Not provided'}
</p>
</div>
</div>
{user?.firstName && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<User className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">First Name</p>
<p className="text-base text-gray-900">{user.firstName}</p>
</div>
</div>
)}
{user?.lastName && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<User className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Last Name</p>
<p className="text-base text-gray-900">{user.lastName}</p>
</div>
</div>
)}
{user?.displayName && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<User className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Display Name</p>
<p className="text-base text-gray-900">{user.displayName}</p>
</div>
</div>
)}
{(user as any)?.phone && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<Phone className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Phone</p>
<p className="text-base text-gray-900">{(user as any).phone}</p>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Professional Information */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-emerald-100 rounded-lg">
<Briefcase className="h-5 w-5 text-emerald-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Professional Information</CardTitle>
<CardDescription className="text-gray-600">Work-related details</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
{user?.employeeId && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<User className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Employee ID</p>
<p className="text-base text-gray-900 font-medium">{user.employeeId}</p>
</div>
</div>
)}
{user?.department && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<Building2 className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Department</p>
<p className="text-base text-gray-900">{user.department}</p>
</div>
</div>
)}
{user?.designation && (
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<Briefcase className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Designation</p>
<p className="text-base text-gray-900">{user.designation}</p>
</div>
</div>
)}
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors">
<Shield className="w-5 h-5 text-gray-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-500">Role</p>
<div className="flex items-center gap-2 mt-1">
<Badge
variant={user?.isAdmin ? "default" : "secondary"}
className={user?.isAdmin ? "bg-yellow-400 text-slate-900" : ""}
>
{user?.isAdmin ? 'Administrator' : 'User'}
</Badge>
{user?.isAdmin && (
<CheckCircle className="w-4 h-4 text-green-600" />
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Account Status Card */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-purple-100 rounded-lg">
<Calendar className="h-5 w-5 text-purple-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Account Status</CardTitle>
<CardDescription className="text-gray-600">Account information and activity</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500 rounded-full">
<CheckCircle className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">Account Status</p>
<p className="text-xs text-gray-600">Active and verified</p>
</div>
</div>
<Badge className="bg-green-500 hover:bg-green-600 text-white">
Active
</Badge>
</div>
{user?.userId && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm font-medium text-gray-500 mb-1">User ID</p>
<p className="text-sm text-gray-900 font-mono break-all">{user.userId}</p>
</div>
{user.employeeId && (
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm font-medium text-gray-500 mb-1">Employee ID</p>
<p className="text-sm text-gray-900 font-mono">{user.employeeId}</p>
</div>
)}
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,2 @@
export { Profile } from './Profile';

View File

@ -0,0 +1,146 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Settings as SettingsIcon,
Bell,
Shield,
Palette,
Globe,
Lock,
Database,
Mail,
CheckCircle
} from 'lucide-react';
export function Settings() {
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header Card */}
<Card className="relative overflow-hidden shadow-xl border-0">
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"></div>
<CardContent className="relative z-10 p-8 lg:p-12">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div className="text-white">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-yellow-400 rounded-xl flex items-center justify-center shadow-lg">
<SettingsIcon className="w-8 h-8 text-slate-900" />
</div>
<div>
<h1 className="text-3xl font-bold mb-2 text-white">Settings</h1>
<p className="text-lg text-gray-200">Manage your account settings and preferences</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Settings Sections */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Notification Settings */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-100 rounded-lg">
<Bell className="h-5 w-5 text-blue-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Notifications</CardTitle>
<CardDescription className="text-gray-600">Manage notification preferences</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">Notification settings will be available soon</p>
</div>
</div>
</CardContent>
</Card>
{/* Security Settings */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-red-100 rounded-lg">
<Lock className="h-5 w-5 text-red-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Security</CardTitle>
<CardDescription className="text-gray-600">Password and security settings</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">Security settings will be available soon</p>
</div>
</div>
</CardContent>
</Card>
{/* Appearance Settings */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-purple-100 rounded-lg">
<Palette className="h-5 w-5 text-purple-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Appearance</CardTitle>
<CardDescription className="text-gray-600">Theme and display preferences</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">Appearance settings will be available soon</p>
</div>
</div>
</CardContent>
</Card>
{/* Preferences */}
<Card className="shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-emerald-100 rounded-lg">
<Shield className="h-5 w-5 text-emerald-600" />
</div>
<div>
<CardTitle className="text-lg text-gray-900">Preferences</CardTitle>
<CardDescription className="text-gray-600">Application preferences</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">User preferences will be available soon</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Coming Soon Notice */}
<Card className="shadow-lg border-yellow-200 bg-yellow-50">
<CardContent className="p-6">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-yellow-600 shrink-0" />
<div>
<p className="text-sm font-medium text-gray-900">Settings page is under development</p>
<p className="text-xs text-gray-600 mt-1">Settings and preferences management will be available in a future update.</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,2 @@
export { Settings } from './Settings';

236
src/services/authApi.ts Normal file
View File

@ -0,0 +1,236 @@
/**
* Authentication API Service
* Handles communication with backend auth endpoints
*/
import axios, { AxiosInstance } from 'axios';
import { TokenManager } from '../utils/tokenManager';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
// Create axios instance with default config
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // Important for cookie-based auth in localhost
});
// Request interceptor to add access token
apiClient.interceptors.request.use(
(config) => {
const token = TokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// If error is 401 and we haven't retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh token
const refreshToken = TokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post(
`${API_BASE_URL}/auth/refresh`,
{ refreshToken },
{ withCredentials: true }
);
const { accessToken } = response.data.data || response.data;
if (accessToken) {
TokenManager.setAccessToken(accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
}
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
TokenManager.clearAll();
window.location.href = '/';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export interface TokenExchangeResponse {
user: {
userId: string;
employeeId: string;
email: string;
firstName: string;
lastName: string;
displayName: string;
department?: string;
designation?: string;
isAdmin: boolean;
};
accessToken: string;
refreshToken: string;
}
export interface RefreshTokenResponse {
accessToken: string;
}
/**
* Exchange authorization code for tokens (localhost only)
*/
export async function exchangeCodeForTokens(
code: string,
redirectUri: string
): Promise<TokenExchangeResponse> {
console.log('🔄 Exchange Code for Tokens:', {
code: code ? `${code.substring(0, 10)}...` : 'MISSING',
redirectUri,
endpoint: `${API_BASE_URL}/auth/token-exchange`,
});
try {
const response = await apiClient.post<TokenExchangeResponse>(
'/auth/token-exchange',
{
code,
redirectUri,
},
{
responseType: 'json', // Explicitly set response type to JSON
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
}
);
console.log('✅ Token exchange successful', {
status: response.status,
statusText: response.statusText,
headers: response.headers,
contentType: response.headers['content-type'],
hasData: !!response.data,
dataType: typeof response.data,
dataIsArray: Array.isArray(response.data),
dataPreview: Array.isArray(response.data)
? `Array[${response.data.length}]`
: typeof response.data === 'object'
? JSON.stringify(response.data).substring(0, 100)
: String(response.data).substring(0, 100),
});
// Check if response is an array (buffer issue)
if (Array.isArray(response.data)) {
console.error('❌ Response is an array (buffer issue):', {
arrayLength: response.data.length,
firstFew: response.data.slice(0, 10),
rawResponse: response,
});
throw new Error('Invalid response format: received array instead of JSON. Check Content-Type header.');
}
const data = response.data as any;
const result = data.data || data;
// Tokens are set as httpOnly cookies by backend, but we also store them here for client access
if (result.accessToken && result.refreshToken) {
TokenManager.setAccessToken(result.accessToken);
TokenManager.setRefreshToken(result.refreshToken);
TokenManager.setUserData(result.user);
console.log('✅ Tokens stored successfully');
} else {
console.warn('⚠️ Tokens missing in response', { result });
}
return result;
} catch (error: any) {
console.error('❌ Token exchange failed:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
code: code ? `${code.substring(0, 10)}...` : 'MISSING',
redirectUri,
});
throw error;
}
}
/**
* Refresh access token using refresh token
*/
export async function refreshAccessToken(): Promise<string> {
const refreshToken = TokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await apiClient.post<RefreshTokenResponse>('/auth/refresh', {
refreshToken,
});
const data = response.data as any;
const accessToken = data.data?.accessToken || data.accessToken;
if (accessToken) {
TokenManager.setAccessToken(accessToken);
return accessToken;
}
throw new Error('Failed to refresh token');
}
/**
* Get current user profile
*/
export async function getCurrentUser() {
const response = await apiClient.get('/auth/me');
const data = response.data as any;
return data.data || data;
}
/**
* Logout user
* CRITICAL: This endpoint MUST clear httpOnly cookies set by backend
* Note: TokenManager.clearAll() is called in AuthContext.logout()
* We don't call it here to avoid double clearing
*/
export async function logout(): Promise<void> {
try {
console.log('📡 Calling backend logout endpoint to clear httpOnly cookies...');
// Use withCredentials to ensure cookies are sent
const response = await apiClient.post('/auth/logout', {}, {
withCredentials: true, // Ensure cookies are sent with request
});
console.log('📡 Backend logout response:', response.status, response.statusText);
console.log('📡 Response headers (check Set-Cookie):', response.headers);
} catch (error: any) {
console.error('📡 Logout API error:', error);
console.error('📡 Error details:', {
message: error.message,
status: error.response?.status,
data: error.response?.data,
});
// Even if API call fails, cookies might still be cleared
// Don't throw - let the caller handle cleanup
}
}
export default apiClient;

400
src/utils/tokenManager.ts Normal file
View File

@ -0,0 +1,400 @@
/**
* Token Management Utilities
* Handles token storage, retrieval, and refresh operations
*/
const ACCESS_TOKEN_KEY = 'accessToken';
const REFRESH_TOKEN_KEY = 'refreshToken';
const USER_DATA_KEY = 'userData';
/**
* Cookie utility functions
*/
export const cookieUtils = {
/**
* Set a cookie with the given name, value, and options
*/
set(name: string, value: string, days: number = 7): void {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
const cookieValue = `${name}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
document.cookie = cookieValue;
},
/**
* Get a cookie by name
*/
get(name: string): string | null {
const nameEQ = name + '=';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i];
while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length);
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length, cookie.length));
}
}
return null;
},
/**
* Remove a cookie
*/
remove(name: string): void {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
},
/**
* Clear all authentication cookies
* Attempts to clear cookies with different paths and domains
*/
clearAll(): void {
const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_DATA_KEY];
cookieNames.forEach(name => {
// Remove with default path
this.remove(name);
// Remove with root path explicitly
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
// Remove with domain (if applicable)
const hostname = window.location.hostname;
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${hostname};`;
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${hostname};`;
}
});
},
};
/**
* Token Manager - Handles token storage and retrieval
*/
export class TokenManager {
/**
* Store access token
*/
static setAccessToken(token: string): void {
if (this.isLocalhost()) {
// Store in cookie for localhost (backend sets httpOnly cookie, but we also store here for client-side access)
localStorage.setItem(ACCESS_TOKEN_KEY, token);
cookieUtils.set(ACCESS_TOKEN_KEY, token, 1); // 1 day
} else {
localStorage.setItem(ACCESS_TOKEN_KEY, token);
}
}
/**
* Get access token
*/
static getAccessToken(): string | null {
if (this.isLocalhost()) {
// Try cookie first (set by backend), then localStorage
return cookieUtils.get(ACCESS_TOKEN_KEY) || localStorage.getItem(ACCESS_TOKEN_KEY);
}
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
/**
* Store refresh token
*/
static setRefreshToken(token: string): void {
if (this.isLocalhost()) {
localStorage.setItem(REFRESH_TOKEN_KEY, token);
cookieUtils.set(REFRESH_TOKEN_KEY, token, 7); // 7 days
} else {
localStorage.setItem(REFRESH_TOKEN_KEY, token);
}
}
/**
* Get refresh token
*/
static getRefreshToken(): string | null {
if (this.isLocalhost()) {
// Try cookie first (set by backend), then localStorage
return cookieUtils.get(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY);
}
return localStorage.getItem(REFRESH_TOKEN_KEY);
}
/**
* Store user data
*/
static setUserData(user: any): void {
localStorage.setItem(USER_DATA_KEY, JSON.stringify(user));
if (this.isLocalhost()) {
cookieUtils.set(USER_DATA_KEY, JSON.stringify(user), 7);
}
}
/**
* Get user data
*/
static getUserData(): any | null {
const data = localStorage.getItem(USER_DATA_KEY);
if (!data) return null;
try {
return JSON.parse(data);
} catch {
return null;
}
}
/**
* Clear all tokens and user data
* This includes localStorage, sessionStorage, cookies, and any auth-related data
* Uses aggressive clearing to ensure ALL data is removed
* IMPORTANT: This also sets a flag to prevent auto-authentication
*/
static clearAll(): void {
console.log('TokenManager.clearAll() - Starting cleanup...');
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
// This flag survives the redirect and prevents auto-authentication
try {
sessionStorage.setItem('__logout_in_progress__', 'true');
sessionStorage.setItem('__force_logout__', 'true');
} catch (e) {
console.warn('Could not set logout flags:', e);
}
// Step 1: Clear specific auth-related localStorage keys
const authKeys = [
ACCESS_TOKEN_KEY,
REFRESH_TOKEN_KEY,
USER_DATA_KEY,
'oktaToken',
'authToken',
'user',
'authState',
'access_token',
'refresh_token',
'id_token',
'token',
'accessToken',
'refreshToken',
'userData',
'auth',
'authentication',
'persist:root',
'persist:auth',
'redux-persist',
];
authKeys.forEach(key => {
try {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
console.log(`Removed ${key} from storage`);
} catch (e) {
console.warn(`Error removing ${key}:`, e);
}
});
// Step 2: Clear ALL localStorage items by iterating and removing
try {
const allLocalStorageKeys: string[] = [];
// Get all keys
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
allLocalStorageKeys.push(key);
}
}
// Remove each key explicitly
allLocalStorageKeys.forEach(key => {
try {
localStorage.removeItem(key);
console.log(`Removed localStorage key: ${key}`);
} catch (e) {
console.warn(`Error removing localStorage key ${key}:`, e);
}
});
// Final clear as backup
localStorage.clear();
console.log('localStorage.clear() called');
} catch (e) {
console.error('Error clearing localStorage:', e);
}
// Step 3: Clear ALL sessionStorage items
try {
const allSessionStorageKeys: string[] = [];
// Get all keys
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) {
allSessionStorageKeys.push(key);
}
}
// Remove each key explicitly
allSessionStorageKeys.forEach(key => {
try {
sessionStorage.removeItem(key);
console.log(`Removed sessionStorage key: ${key}`);
} catch (e) {
console.warn(`Error removing sessionStorage key ${key}:`, e);
}
});
// Final clear as backup
sessionStorage.clear();
console.log('sessionStorage.clear() called');
} catch (e) {
console.error('Error clearing sessionStorage:', e);
}
// Step 4: Clear cookies (both client-side and attempt to clear httpOnly cookies)
cookieUtils.clearAll();
// Step 5: Aggressively clear ALL cookies including httpOnly ones
// Note: httpOnly cookies can only be cleared by backend, but we try everything
const cookieNames = [
'accessToken', 'refreshToken', 'userData', 'oktaToken', 'authToken',
'id_token', 'token', 'access_token', 'refresh_token'
];
const hostname = window.location.hostname;
const paths = ['/', '/login', '/login/callback', '/api', '/api/v1'];
const domains = ['', hostname, `.${hostname}`];
// Try every combination of path, domain, and secure flags
cookieNames.forEach(name => {
paths.forEach(path => {
domains.forEach(domain => {
// Try without secure flag
try {
let cookieString = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=Lax;`;
if (domain && domain !== 'localhost' && domain !== '127.0.0.1' && !domain.startsWith('.')) {
cookieString += ` domain=${domain};`;
}
document.cookie = cookieString;
} catch (e) {
// Ignore errors
}
// Try with secure flag
try {
let cookieString = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure;`;
if (domain && domain !== 'localhost' && domain !== '127.0.0.1') {
cookieString += ` domain=${domain};`;
}
document.cookie = cookieString;
} catch (e) {
// Ignore errors
}
// Try with SameSite=Strict
try {
let cookieString = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=Strict;`;
if (domain && domain !== 'localhost' && domain !== '127.0.0.1' && !domain.startsWith('.')) {
cookieString += ` domain=${domain};`;
}
document.cookie = cookieString;
} catch (e) {
// Ignore errors
}
});
});
});
// Log remaining cookies (httpOnly cookies will still show here as they can't be cleared from JS)
console.log('⚠️ Remaining cookies (httpOnly cookies cannot be cleared from JavaScript):', document.cookie);
console.log('⚠️ httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this');
// Step 6: Verify cleanup
console.log('TokenManager.clearAll() - Verification:');
console.log(`localStorage length: ${localStorage.length}`);
console.log(`sessionStorage length: ${sessionStorage.length}`);
console.log(`Cookies: ${document.cookie}`);
if (localStorage.length > 0 || sessionStorage.length > 0) {
console.warn('WARNING: Storage not fully cleared!');
console.log('Remaining localStorage keys:', Object.keys(localStorage));
console.log('Remaining sessionStorage keys:', Object.keys(sessionStorage));
}
console.log('TokenManager.clearAll() - Cleanup complete');
}
/**
* Check if access token exists
*/
static hasAccessToken(): boolean {
return !!this.getAccessToken();
}
/**
* Check if refresh token exists
*/
static hasRefreshToken(): boolean {
return !!this.getRefreshToken();
}
/**
* Check if we're running on localhost
*/
static isLocalhost(): boolean {
return (
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname === ''
);
}
}
/**
* Cookie utility for browser-side cookie management
* Note: Backend sets httpOnly cookies, these utilities are for client-side access if needed
*/
export const CookieManager = {
set(name: string, value: string, days: number = 7): void {
cookieUtils.set(name, value, days);
},
get(name: string): string | null {
return cookieUtils.get(name);
},
remove(name: string): void {
cookieUtils.remove(name);
},
clearAll(): void {
cookieUtils.clearAll();
},
};
/**
* Check if token is expired (with buffer time)
*/
export function isTokenExpired(token: string | null, bufferMinutes: number = 5): boolean {
if (!token) return true;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const exp = payload.exp * 1000; // Convert to milliseconds
const now = Date.now();
const buffer = bufferMinutes * 60 * 1000;
return exp - now < buffer;
} catch {
return true;
}
}
/**
* Get token expiration time
*/
export function getTokenExpiration(token: string | null): Date | null {
if (!token) return null;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return new Date(payload.exp * 1000);
} catch {
return null;
}
}