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 { CreateRequest } from '@/pages/CreateRequest';
|
||||||
import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
||||||
import { MyRequests } from '@/pages/MyRequests';
|
import { MyRequests } from '@/pages/MyRequests';
|
||||||
|
import { Profile } from '@/pages/Profile';
|
||||||
|
import { Settings } from '@/pages/Settings';
|
||||||
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -55,6 +57,15 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
}, [dynamicRequests]);
|
}, [dynamicRequests]);
|
||||||
|
|
||||||
const handleNavigate = (page: string) => {
|
const handleNavigate = (page: string) => {
|
||||||
|
// Handle special routes
|
||||||
|
if (page === 'profile') {
|
||||||
|
navigate('/profile');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (page === 'settings') {
|
||||||
|
navigate('/settings');
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigate(`/${page}`);
|
navigate(`/${page}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -556,6 +567,26 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Profile */}
|
||||||
|
<Route
|
||||||
|
path="/profile"
|
||||||
|
element={
|
||||||
|
<PageLayout currentPage="profile" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
||||||
|
<Profile />
|
||||||
|
</PageLayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<PageLayout currentPage="settings" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
||||||
|
<Settings />
|
||||||
|
</PageLayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
<Toaster
|
<Toaster
|
||||||
@ -585,10 +616,18 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main App Component with Router
|
// Main App Component with Router
|
||||||
export default function App() {
|
interface MainAppProps {
|
||||||
|
onLogout?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App(props?: MainAppProps) {
|
||||||
|
const { onLogout } = props || {};
|
||||||
|
console.log('🟢 Main App component rendered');
|
||||||
|
console.log('🟢 onLogout prop received:', !!onLogout);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppRoutes />
|
<AppRoutes onLogout={onLogout} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,17 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import royalEnfieldLogo from '@/assets/images/royal_enfield_logo.png';
|
import royalEnfieldLogo from '@/assets/images/royal_enfield_logo.png';
|
||||||
|
|
||||||
interface PageLayoutProps {
|
interface PageLayoutProps {
|
||||||
@ -17,6 +28,23 @@ interface PageLayoutProps {
|
|||||||
|
|
||||||
export function PageLayout({ children, currentPage = 'dashboard', onNavigate, onNewRequest, onLogout }: PageLayoutProps) {
|
export function PageLayout({ children, currentPage = 'dashboard', onNavigate, onNewRequest, onLogout }: PageLayoutProps) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Get user initials for avatar
|
||||||
|
const getUserInitials = () => {
|
||||||
|
if (user?.displayName) {
|
||||||
|
const names = user.displayName.split(' ').filter(Boolean);
|
||||||
|
if (names.length >= 2) {
|
||||||
|
return `${names[0]?.[0] || ''}${names[names.length - 1]?.[0] || ''}`.toUpperCase();
|
||||||
|
}
|
||||||
|
return user.displayName.substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
if (user?.email) {
|
||||||
|
return user.email.substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
return 'U';
|
||||||
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: Home },
|
{ id: 'dashboard', label: 'Dashboard', icon: Home },
|
||||||
@ -188,20 +216,32 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Avatar className="cursor-pointer shrink-0 h-10 w-10">
|
<Avatar className="cursor-pointer shrink-0 h-10 w-10">
|
||||||
<AvatarImage src="" />
|
<AvatarImage src={user?.picture || ''} />
|
||||||
<AvatarFallback className="bg-re-green text-white text-sm">JD</AvatarFallback>
|
<AvatarFallback className="bg-re-green text-white text-sm">
|
||||||
|
{getUserInitials()}
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem onClick={() => onNavigate?.('profile')}>
|
||||||
<User className="w-4 h-4 mr-2" />
|
<User className="w-4 h-4 mr-2" />
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Settings page not ready yet - do nothing for now
|
||||||
|
}}
|
||||||
|
className="opacity-50 cursor-not-allowed"
|
||||||
|
>
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
Settings
|
Settings
|
||||||
|
<span className="ml-auto text-xs text-gray-400">Coming soon</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={onLogout} className="text-red-600 focus:text-red-600">
|
<DropdownMenuItem
|
||||||
|
onClick={() => setShowLogoutDialog(true)}
|
||||||
|
className="text-red-600 focus:text-red-600"
|
||||||
|
>
|
||||||
<LogOut className="w-4 h-4 mr-2" />
|
<LogOut className="w-4 h-4 mr-2" />
|
||||||
Logout
|
Logout
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -215,6 +255,48 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Logout Confirmation Dialog */}
|
||||||
|
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<LogOut className="w-5 h-5 text-red-600" />
|
||||||
|
Confirm Logout
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="pt-2">
|
||||||
|
Are you sure you want to logout? You will need to sign in again to access your account.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setShowLogoutDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
console.log('🔴 Logout button clicked in PageLayout');
|
||||||
|
console.log('🔴 onLogout function exists?', !!onLogout);
|
||||||
|
setShowLogoutDialog(false);
|
||||||
|
if (onLogout) {
|
||||||
|
console.log('🔴 Calling onLogout function...');
|
||||||
|
try {
|
||||||
|
await onLogout();
|
||||||
|
console.log('🔴 onLogout completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('🔴 Error calling onLogout:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('🔴 ERROR: onLogout is undefined!');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white focus:ring-red-600"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4 mr-2" />
|
||||||
|
Logout
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -28,40 +28,40 @@ function AlertDialogPortal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<AlertDialogPrimitive.Overlay
|
||||||
<AlertDialogPrimitive.Overlay
|
data-slot="alert-dialog-overlay"
|
||||||
data-slot="alert-dialog-overlay"
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
</AlertDialogPortal>
|
||||||
}
|
));
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||||
function AlertDialogContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
data-slot="alert-dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogHeader({
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
|
|||||||
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 React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { Auth0Provider } from '@auth0/auth0-react';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { AuthenticatedApp } from './pages/Auth';
|
import { AuthenticatedApp } from './pages/Auth';
|
||||||
import './styles/globals.css';
|
import './styles/globals.css';
|
||||||
|
|
||||||
console.log('Application Starting...');
|
console.log('Application Starting...');
|
||||||
console.log('Auth0 Configuration:', {
|
console.log('Environment:', {
|
||||||
domain: 'dev-dq1hiere8khcbdra.us.auth0.com',
|
hostname: window.location.hostname,
|
||||||
clientId: '5gnC2Go5yRg3RXLuaFLr8KRc1fJ9PDJW',
|
origin: window.location.origin,
|
||||||
origin: window.location.origin
|
isLocalhost: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Auth0Provider
|
<AuthProvider>
|
||||||
domain="dev-dq1hiere8khcbdra.us.auth0.com"
|
|
||||||
clientId="5gnC2Go5yRg3RXLuaFLr8KRc1fJ9PDJW"
|
|
||||||
authorizationParams={{
|
|
||||||
redirect_uri: 'https://9b89f4bfd360.ngrok-free.app'
|
|
||||||
}}
|
|
||||||
onRedirectCallback={(appState) => {
|
|
||||||
console.log('Auth0 Redirect Callback:', {
|
|
||||||
appState,
|
|
||||||
returnTo: appState?.returnTo || window.location.pathname,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AuthenticatedApp />
|
<AuthenticatedApp />
|
||||||
</Auth0Provider>
|
</AuthProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { LogIn, Shield } from 'lucide-react';
|
import { LogIn, Shield } from 'lucide-react';
|
||||||
|
|
||||||
export function Auth() {
|
export function Auth() {
|
||||||
const { loginWithRedirect, isLoading, error } = useAuth0();
|
const { login, isLoading, error } = useAuth();
|
||||||
|
|
||||||
console.log('Auth Component Render - Auth0 State:', {
|
console.log('Auth Component Render - Auth0 State:', {
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -25,26 +25,8 @@ export function Auth() {
|
|||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
console.log('Storage cleared');
|
console.log('Storage cleared');
|
||||||
|
|
||||||
console.log('Calling loginWithRedirect with params:', {
|
|
||||||
authorizationParams: {
|
|
||||||
screen_hint: 'login',
|
|
||||||
prompt: 'login',
|
|
||||||
mode: 'login'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginWithRedirect({
|
await login();
|
||||||
authorizationParams: {
|
|
||||||
screen_hint: 'login',
|
|
||||||
prompt: 'login',
|
|
||||||
mode: 'login'
|
|
||||||
},
|
|
||||||
appState: {
|
|
||||||
returnTo: window.location.pathname
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Login redirect initiated successfully');
|
console.log('Login redirect initiated successfully');
|
||||||
} catch (loginError) {
|
} catch (loginError) {
|
||||||
console.error('========================================');
|
console.error('========================================');
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { Loader } from '@/components/common/Loader';
|
import { Loader } from '@/components/common/Loader';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function AuthCallback() {
|
export function AuthCallback() {
|
||||||
const { isAuthenticated, isLoading, error, user } = useAuth0();
|
const { isAuthenticated, isLoading, error, user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('AuthCallback Component Mounted');
|
console.log('AuthCallback Component Mounted');
|
||||||
console.log('Auth0 State during callback:', {
|
console.log('Auth State during callback:', {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
@ -17,19 +19,20 @@ export function AuthCallback() {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
console.log('User authenticated successfully:', {
|
console.log('User authenticated successfully:', {
|
||||||
id: user.sub,
|
userId: user.userId,
|
||||||
|
employeeId: user.employeeId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
displayName: user.displayName,
|
||||||
picture: user.picture,
|
|
||||||
nickname: user.nickname,
|
|
||||||
userData: user
|
userData: user
|
||||||
});
|
});
|
||||||
|
// Redirect to home after successful authentication
|
||||||
|
navigate('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error during authentication callback:', error);
|
console.error('Error during authentication callback:', error);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isLoading, error, user]);
|
}, [isAuthenticated, isLoading, error, user, navigate]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('AuthCallback Error:', error);
|
console.error('AuthCallback Error:', error);
|
||||||
|
|||||||
@ -1,37 +1,42 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { Auth } from './Auth';
|
import { Auth } from './Auth';
|
||||||
import { AuthCallback } from './AuthCallback';
|
import { AuthCallback } from './AuthCallback';
|
||||||
import { AuthDebugInfo } from '@/components/Auth/AuthDebugInfo';
|
import { AuthDebugInfo } from '@/components/Auth/AuthDebugInfo';
|
||||||
import App from '../../App';
|
import App from '../../App';
|
||||||
|
|
||||||
export function AuthenticatedApp() {
|
export function AuthenticatedApp() {
|
||||||
const { isAuthenticated, isLoading, error, user, logout: logoutAuth0 } = useAuth0();
|
const { isAuthenticated, isLoading, error, user, logout } = useAuth();
|
||||||
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
console.log('========================================');
|
console.log('🔵 ========================================');
|
||||||
console.log('LOGOUT - Initiated');
|
console.log('🔵 AuthenticatedApp.handleLogout - CALLED');
|
||||||
console.log('Timestamp:', new Date().toISOString());
|
console.log('🔵 Timestamp:', new Date().toISOString());
|
||||||
console.log('========================================');
|
console.log('🔵 logout function exists?', !!logout);
|
||||||
|
console.log('🔵 ========================================');
|
||||||
// Clear all storage
|
|
||||||
console.log('Clearing all storage...');
|
|
||||||
localStorage.clear();
|
|
||||||
sessionStorage.clear();
|
|
||||||
console.log('Storage cleared');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await logoutAuth0({
|
if (!logout) {
|
||||||
logoutParams: {
|
console.error('🔵 ERROR: logout function is undefined!');
|
||||||
returnTo: window.location.origin
|
return;
|
||||||
},
|
}
|
||||||
localOnly: false // Clear Auth0 session as well
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Logout successful');
|
console.log('🔵 Calling logout from auth context...');
|
||||||
|
// Call logout from auth context (handles all cleanup and redirect)
|
||||||
|
await logout();
|
||||||
|
console.log('🔵 Logout successful - redirecting to login...');
|
||||||
} catch (logoutError) {
|
} catch (logoutError) {
|
||||||
console.error('Logout error:', logoutError);
|
console.error('🔵 Logout error in handleLogout:', logoutError);
|
||||||
|
// Even if logout fails, clear local data and redirect
|
||||||
|
try {
|
||||||
|
console.log('🔵 Attempting emergency cleanup...');
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('🔵 Error during emergency cleanup:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,15 +53,18 @@ export function AuthenticatedApp() {
|
|||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('USER AUTHENTICATED - Full Details');
|
console.log('USER AUTHENTICATED - Full Details');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('User ID (sub):', user.sub);
|
console.log('User ID:', user.userId || user.sub);
|
||||||
|
console.log('Employee ID:', user.employeeId);
|
||||||
console.log('Email:', user.email);
|
console.log('Email:', user.email);
|
||||||
console.log('Name:', user.name);
|
console.log('Name:', user.displayName || user.name);
|
||||||
console.log('Nickname:', user.nickname);
|
console.log('Display Name:', user.displayName);
|
||||||
console.log('Picture:', user.picture);
|
console.log('First Name:', user.firstName);
|
||||||
console.log('Email Verified:', user.email_verified);
|
console.log('Last Name:', user.lastName);
|
||||||
console.log('Updated At:', user.updated_at);
|
console.log('Department:', user.department);
|
||||||
|
console.log('Designation:', user.designation);
|
||||||
|
console.log('Is Admin:', user.isAdmin);
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('ALL USER CLAIMS:');
|
console.log('ALL USER DATA:');
|
||||||
console.log(JSON.stringify(user, null, 2));
|
console.log(JSON.stringify(user, null, 2));
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
}
|
}
|
||||||
|
|||||||
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