login flow added
This commit is contained in:
parent
913a8d2c1b
commit
96d8bedf6d
969
.cursor/project_setup.md
Normal file
969
.cursor/project_setup.md
Normal 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
|
||||
|
||||
43
src/App.tsx
43
src/App.tsx
@ -9,6 +9,8 @@ import { WorkNoteChat } from '@/components/workNote/WorkNoteChat';
|
||||
import { CreateRequest } from '@/pages/CreateRequest';
|
||||
import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
||||
import { MyRequests } from '@/pages/MyRequests';
|
||||
import { Profile } from '@/pages/Profile';
|
||||
import { Settings } from '@/pages/Settings';
|
||||
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { toast } from 'sonner';
|
||||
@ -55,6 +57,15 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
}, [dynamicRequests]);
|
||||
|
||||
const handleNavigate = (page: string) => {
|
||||
// Handle special routes
|
||||
if (page === 'profile') {
|
||||
navigate('/profile');
|
||||
return;
|
||||
}
|
||||
if (page === 'settings') {
|
||||
navigate('/settings');
|
||||
return;
|
||||
}
|
||||
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>
|
||||
|
||||
<Toaster
|
||||
@ -585,10 +616,18 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
<AppRoutes onLogout={onLogout} />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,17 @@ import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
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';
|
||||
|
||||
interface PageLayoutProps {
|
||||
@ -17,6 +28,23 @@ interface PageLayoutProps {
|
||||
|
||||
export function PageLayout({ children, currentPage = 'dashboard', onNavigate, onNewRequest, onLogout }: PageLayoutProps) {
|
||||
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 = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: Home },
|
||||
@ -188,20 +216,32 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="cursor-pointer shrink-0 h-10 w-10">
|
||||
<AvatarImage src="" />
|
||||
<AvatarFallback className="bg-re-green text-white text-sm">JD</AvatarFallback>
|
||||
<AvatarImage src={user?.picture || ''} />
|
||||
<AvatarFallback className="bg-re-green text-white text-sm">
|
||||
{getUserInitials()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onNavigate?.('profile')}>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
</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
|
||||
<span className="ml-auto text-xs text-gray-400">Coming soon</span>
|
||||
</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
|
||||
</DropdownMenuItem>
|
||||
@ -215,6 +255,48 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
||||
{children}
|
||||
</main>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -28,11 +28,10 @@ function AlertDialogPortal({
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
@ -40,19 +39,20 @@ function AlertDialogOverlay({
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
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(
|
||||
"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,
|
||||
@ -60,8 +60,8 @@ function AlertDialogContent({
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
|
||||
618
src/contexts/AuthContext.tsx
Normal file
618
src/contexts/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
|
||||
27
src/main.tsx
27
src/main.tsx
@ -1,34 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Auth0Provider } from '@auth0/auth0-react';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { AuthenticatedApp } from './pages/Auth';
|
||||
import './styles/globals.css';
|
||||
|
||||
console.log('Application Starting...');
|
||||
console.log('Auth0 Configuration:', {
|
||||
domain: 'dev-dq1hiere8khcbdra.us.auth0.com',
|
||||
clientId: '5gnC2Go5yRg3RXLuaFLr8KRc1fJ9PDJW',
|
||||
origin: window.location.origin
|
||||
console.log('Environment:', {
|
||||
hostname: window.location.hostname,
|
||||
origin: window.location.origin,
|
||||
isLocalhost: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1',
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Auth0Provider
|
||||
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()
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AuthProvider>
|
||||
<AuthenticatedApp />
|
||||
</Auth0Provider>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { LogIn, Shield } from 'lucide-react';
|
||||
|
||||
export function Auth() {
|
||||
const { loginWithRedirect, isLoading, error } = useAuth0();
|
||||
const { login, isLoading, error } = useAuth();
|
||||
|
||||
console.log('Auth Component Render - Auth0 State:', {
|
||||
isLoading,
|
||||
@ -25,26 +25,8 @@ export function Auth() {
|
||||
sessionStorage.clear();
|
||||
console.log('Storage cleared');
|
||||
|
||||
console.log('Calling loginWithRedirect with params:', {
|
||||
authorizationParams: {
|
||||
screen_hint: 'login',
|
||||
prompt: 'login',
|
||||
mode: 'login'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await loginWithRedirect({
|
||||
authorizationParams: {
|
||||
screen_hint: 'login',
|
||||
prompt: 'login',
|
||||
mode: 'login'
|
||||
},
|
||||
appState: {
|
||||
returnTo: window.location.pathname
|
||||
}
|
||||
});
|
||||
|
||||
await login();
|
||||
console.log('Login redirect initiated successfully');
|
||||
} catch (loginError) {
|
||||
console.error('========================================');
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Loader } from '@/components/common/Loader';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function AuthCallback() {
|
||||
const { isAuthenticated, isLoading, error, user } = useAuth0();
|
||||
const { isAuthenticated, isLoading, error, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AuthCallback Component Mounted');
|
||||
console.log('Auth0 State during callback:', {
|
||||
console.log('Auth State during callback:', {
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
error: error?.message,
|
||||
@ -17,19 +19,20 @@ export function AuthCallback() {
|
||||
|
||||
if (user) {
|
||||
console.log('User authenticated successfully:', {
|
||||
id: user.sub,
|
||||
userId: user.userId,
|
||||
employeeId: user.employeeId,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
picture: user.picture,
|
||||
nickname: user.nickname,
|
||||
displayName: user.displayName,
|
||||
userData: user
|
||||
});
|
||||
// Redirect to home after successful authentication
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Error during authentication callback:', error);
|
||||
}
|
||||
}, [isAuthenticated, isLoading, error, user]);
|
||||
}, [isAuthenticated, isLoading, error, user, navigate]);
|
||||
|
||||
if (error) {
|
||||
console.error('AuthCallback Error:', error);
|
||||
|
||||
@ -1,37 +1,42 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Auth } from './Auth';
|
||||
import { AuthCallback } from './AuthCallback';
|
||||
import { AuthDebugInfo } from '@/components/Auth/AuthDebugInfo';
|
||||
import App from '../../App';
|
||||
|
||||
export function AuthenticatedApp() {
|
||||
const { isAuthenticated, isLoading, error, user, logout: logoutAuth0 } = useAuth0();
|
||||
const { isAuthenticated, isLoading, error, user, logout } = useAuth();
|
||||
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
console.log('========================================');
|
||||
console.log('LOGOUT - Initiated');
|
||||
console.log('Timestamp:', new Date().toISOString());
|
||||
console.log('========================================');
|
||||
|
||||
// Clear all storage
|
||||
console.log('Clearing all storage...');
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
console.log('Storage cleared');
|
||||
console.log('🔵 ========================================');
|
||||
console.log('🔵 AuthenticatedApp.handleLogout - CALLED');
|
||||
console.log('🔵 Timestamp:', new Date().toISOString());
|
||||
console.log('🔵 logout function exists?', !!logout);
|
||||
console.log('🔵 ========================================');
|
||||
|
||||
try {
|
||||
await logoutAuth0({
|
||||
logoutParams: {
|
||||
returnTo: window.location.origin
|
||||
},
|
||||
localOnly: false // Clear Auth0 session as well
|
||||
});
|
||||
if (!logout) {
|
||||
console.error('🔵 ERROR: logout function is undefined!');
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
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('USER AUTHENTICATED - Full Details');
|
||||
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('Name:', user.name);
|
||||
console.log('Nickname:', user.nickname);
|
||||
console.log('Picture:', user.picture);
|
||||
console.log('Email Verified:', user.email_verified);
|
||||
console.log('Updated At:', user.updated_at);
|
||||
console.log('Name:', user.displayName || user.name);
|
||||
console.log('Display Name:', user.displayName);
|
||||
console.log('First Name:', user.firstName);
|
||||
console.log('Last Name:', user.lastName);
|
||||
console.log('Department:', user.department);
|
||||
console.log('Designation:', user.designation);
|
||||
console.log('Is Admin:', user.isAdmin);
|
||||
console.log('========================================');
|
||||
console.log('ALL USER CLAIMS:');
|
||||
console.log('ALL USER DATA:');
|
||||
console.log(JSON.stringify(user, null, 2));
|
||||
console.log('========================================');
|
||||
}
|
||||
|
||||
281
src/pages/Profile/Profile.tsx
Normal file
281
src/pages/Profile/Profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
2
src/pages/Profile/index.ts
Normal file
2
src/pages/Profile/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Profile } from './Profile';
|
||||
|
||||
146
src/pages/Settings/Settings.tsx
Normal file
146
src/pages/Settings/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
2
src/pages/Settings/index.ts
Normal file
2
src/pages/Settings/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Settings } from './Settings';
|
||||
|
||||
236
src/services/authApi.ts
Normal file
236
src/services/authApi.ts
Normal 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
400
src/utils/tokenManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user