Files
LynkedUpPro_CRM/docs/integration/01_auth_integration.md
T
Satyam 7694788387 feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals
- Complete ProCanvas redesign with retro sports game aesthetic
- Card-game style photo frame with tilt, shimmer, corner star accents
- Daily Missions card with weekly challenge + individual quest progress bars
- Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting)
- Each log action opens themed modal with relevant input fields
- Challenges modal with 6 active challenges and progress tracking
- Achievements modal with all badges, unlock status, descriptions
- Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals
- Rewards & Checkpoints with named stages (Daily Grind to Legend Run)
- Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings)
- Hit The Map button with smooth pulsing glow animation
- Grid pattern overlay in Leaderboard card
- Light mode support via dark: Tailwind variants throughout
- Top 3 badges displayed on profile card
- Fixed dropdown option visibility in dark mode
2026-02-26 01:36:36 +05:30

423 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# I1 — Auth Integration
**Module:** I1 — Replace Mock AuthContext with Real JWT Flow
**Companion backend doc:** `docs/backend/01_authentication_module.md` (B1)
**Depends on:** I0 (API client + interceptors must be set up first)
**Modifies:** `src/context/AuthContext.jsx`, `src/pages/Login.jsx`, `src/App.jsx`
**Creates:** `src/api/auth.js`
---
## 1. Overview
The current `AuthContext.jsx` authenticates users by searching `mockStore.users` in memory. There is no session persistence — a page refresh loses the logged-in user entirely.
After I1, authentication will:
- Call `POST /api/v1/auth/login` → backend validates credentials, sets two `httpOnly` cookies
- Call `GET /api/v1/auth/me` on app mount → restores the session from the cookie without any extra login prompt
- Call `POST /api/v1/auth/logout` → clears cookies and invalidates the refresh token in the DB
- Handle forced logout events dispatched by `interceptors.js` when the refresh token is also expired
### What does NOT change
The public API of `useAuth()` is preserved so that no other component needs to be touched:
| Value / function | Before | After | Notes |
|-----------------|--------|-------|-------|
| `user` | User object from mock store | User object from `GET /auth/me` | Field names differ — see Section 7 |
| `isAuthenticated` | `boolean` | `boolean` | Identical |
| `login(id, pass, type)` | Synchronous, returns `{success, role}` | **Async**, returns `{success, role}` | `Login.jsx` must `await` it |
| `logout()` | Synchronous, clears state | **Async**, calls API then clears state | No changes needed in callers |
| `isLoading` | Does not exist | `boolean``true` until mount check resolves | `ProtectedRoute` must guard on this |
### What is removed
- `useMockStore` import from `AuthContext.jsx`
- `updateProfile()` stub (it was non-functional; will be re-added properly in I4)
---
## 2. Files Summary
| File | Action |
|------|--------|
| `src/api/auth.js` | **Create** — thin API wrapper for all auth endpoints |
| `src/context/AuthContext.jsx` | **Replace** — remove mock logic, add real API calls + session restore |
| `src/pages/Login.jsx` | **Patch**`handleLogin` → async, correct ADMIN redirect, add submit loading state |
| `src/App.jsx` | **Patch**`ProtectedRoute` must handle `isLoading` before redirecting |
---
## 3. `src/api/auth.js` — New File
This is the thin API wrapper. No state, no side effects — just HTTP calls.
```js
import apiClient from './client';
/**
* POST /auth/login
* Returns: { user: UserPublic, message: string }
* Sets: access_token + refresh_token httpOnly cookies
*/
export async function loginApi(identifier, password, type) {
const { data } = await apiClient.post('/auth/login', { identifier, password, type });
return data;
}
/**
* GET /auth/me
* Returns: UserPublic
* Used on mount to restore session from cookie
*/
export async function getMeApi() {
const { data } = await apiClient.get('/auth/me');
return data;
}
/**
* POST /auth/logout
* Returns: { message: string }
* Clears cookies + invalidates refresh_token_hash in DB
*/
export async function logoutApi() {
const { data } = await apiClient.post('/auth/logout');
return data;
}
```
---
## 4. `src/context/AuthContext.jsx` — Full Replacement
Replace the entire file with the implementation below. Key changes:
1. Remove `useMockStore` import
2. Add `isLoading` state (starts `true`, set `false` after mount check)
3. Add `useEffect` for session restore — calls `getMeApi()` on mount
4. Add `useEffect` for forced-logout event from `interceptors.js`
5. `login()` becomes `async`, calls `loginApi()`
6. `logout()` becomes `async`, calls `logoutApi()`
7. `isLoading` is now exported in context value
```jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { logger } from '../utils/logger';
import { toast } from 'sonner';
import { loginApi, getMeApi, logoutApi } from '../api/auth';
const AuthContext = createContext();
export const ROLES = {
OWNER: 'OWNER',
ADMIN: 'ADMIN',
CONTRACTOR: 'CONTRACTOR',
SUBCONTRACTOR: 'SUBCONTRACTOR',
VENDOR: 'VENDOR',
FIELD_AGENT: 'FIELD_AGENT',
CUSTOMER: 'CUSTOMER',
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true); // true until mount check resolves
// ── Session Restore on Mount ──────────────────────────────────────────────
// Calls GET /auth/me. If the access_token cookie is valid, restores the user.
// If expired, the interceptor attempts /auth/refresh automatically.
// If refresh also fails, this catch block runs and isLoading is set false.
useEffect(() => {
let cancelled = false;
async function restoreSession() {
try {
const userData = await getMeApi();
if (!cancelled) {
setUser(userData);
setIsAuthenticated(true);
logger.info('Session restored', { userId: userData.id, role: userData.role });
}
} catch {
// No valid session — that's fine, user will see login page
if (!cancelled) {
setUser(null);
setIsAuthenticated(false);
}
} finally {
if (!cancelled) setIsLoading(false);
}
}
restoreSession();
return () => { cancelled = true; };
}, []);
// ── Forced Logout Listener ────────────────────────────────────────────────
// Triggered by interceptors.js when the refresh token is also expired/invalid.
useEffect(() => {
const handleForceLogout = () => {
setUser(null);
setIsAuthenticated(false);
toast.error('Session expired', {
description: 'Please sign in again.',
});
logger.info('Forced logout — refresh token invalid');
};
window.addEventListener('auth:logout-required', handleForceLogout);
return () => window.removeEventListener('auth:logout-required', handleForceLogout);
}, []);
// ── Login ─────────────────────────────────────────────────────────────────
// Returns { success: true, role } or { success: false, message }
// Callers must await this function.
const login = async (identifier, password, type) => {
try {
const data = await loginApi(identifier, password, type);
setUser(data.user);
setIsAuthenticated(true);
logger.info('User logged in', { userId: data.user.id, role: data.user.role });
toast.success(`Welcome back, ${data.user.full_name}!`);
return { success: true, role: data.user.role };
} catch (error) {
const message = error.response?.data?.detail ?? 'Invalid credentials';
logger.warn('Failed login attempt', { identifier, type });
toast.error('Invalid credentials', {
description: 'Please check your details and try again.',
});
return { success: false, message };
}
};
// ── Logout ────────────────────────────────────────────────────────────────
// Calls API first, then clears local state regardless of API result.
const logout = async () => {
try {
await logoutApi();
} catch (error) {
// API may fail if token already expired — still clear local state
logger.warn('Logout API error (clearing state anyway)', error);
} finally {
setUser(null);
setIsAuthenticated(false);
logger.info('User logged out', { userId: user?.id });
toast.info('Logged out successfully');
}
};
return (
<AuthContext.Provider value={{ user, isAuthenticated, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
```
---
## 5. `src/pages/Login.jsx` — Minimal Patches
Only three changes needed. The UI, tabs, form, and `fillDemo()` function are **untouched**.
### Change 1 — `handleLogin` becomes async
```jsx
// BEFORE
const handleLogin = (e) => {
e.preventDefault();
setError('');
if (!identifier || !password) { setError('Please fill in all fields'); return; }
const result = login(identifier, password, loginType);
if (result.success) { /* switch */ }
else { setError(result.message); }
};
// AFTER
const [isSubmitting, setIsSubmitting] = useState(false); // add this state
const handleLogin = async (e) => {
e.preventDefault();
setError('');
if (!identifier || !password) {
setError('Please fill in all fields');
return;
}
setIsSubmitting(true);
const result = await login(identifier, password, loginType);
setIsSubmitting(false);
if (result.success) {
switch (result.role) {
case 'CUSTOMER': navigate('/portal/profile'); break; // was '/' (bug fix)
case 'OWNER': navigate('/owner/snapshot'); break;
case 'ADMIN': navigate('/admin/dashboard'); break; // was missing (fell to default)
case 'CONTRACTOR': navigate('/contractor/dashboard'); break;
case 'VENDOR': navigate('/vendor/dashboard'); break;
case 'SUBCONTRACTOR': navigate('/subcontractor/dashboard'); break;
default: navigate('/emp/fa/dashboard'); break; // FIELD_AGENT
}
} else {
setError(result.message);
}
};
```
### Change 2 — Disable submit button while submitting
Pass `isSubmitting` to the `RainbowButton` so it can't be double-clicked:
```jsx
<RainbowButton
type="submit"
disabled={isSubmitting}
className="mt-4 text-white py-4 md:py-5 text-base md:text-lg"
>
{isSubmitting ? <span>Signing in</span> : <><span>Sign In</span><ArrowRight size={20} /></>}
</RainbowButton>
```
### Two Redirect Bugs Fixed by I1
| Role | Old redirect (mock) | Correct redirect (real API) |
|------|--------------------|-----------------------------|
| `CUSTOMER` | `/` (Landing page) | `/portal/profile` |
| `ADMIN` | `/emp/fa/dashboard` (fell to default) | `/admin/dashboard` |
These were harmless with mock data (ADMIN can access both routes) but must be corrected for the real system.
---
## 6. `src/App.jsx` — `ProtectedRoute` Patch
Add the `isLoading` guard. Without it, every page refresh causes a flash-to-login before the session check completes.
```jsx
// BEFORE
const ProtectedRoute = ({ children, allowedRoles }) => {
const { user, isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (allowedRoles && !allowedRoles.includes(user.role)) {
return <Navigate to="/" replace />;
}
return children;
};
// AFTER — add isLoading check
const ProtectedRoute = ({ children, allowedRoles }) => {
const { user, isAuthenticated, isLoading } = useAuth();
const location = useLocation();
// Wait for session restore before making any auth decisions
if (isLoading) {
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" />
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (allowedRoles && !allowedRoles.includes(user.role)) {
return <Navigate to="/" replace />;
}
return children;
};
```
No other changes to `App.jsx`.
---
## 7. User Object Field Mapping
The mock store user shape uses camelCase. The real API (`UserPublic` schema from B1) uses snake_case. Components that read from `user` directly may need updates.
| Field purpose | Mock store field | Real API field (`UserPublic`) |
|--------------|-----------------|-------------------------------|
| Display name | `user.name` | `user.full_name` |
| User UUID | `user.id` | `user.id` (same — UUID string) |
| Role string | `user.role` | `user.role` (same) |
| Employee ID | `user.empId` | `user.emp_id` |
| Legacy mock ID | `user.legacyId` / `user.id` (e.g., `'e1'`) | `user.legacy_id` |
| XP points | `user.xp` | `user.xp` (same) |
| Streak | `user.streak` | `user.streak_days` |
| Achievements | `user.achievements` | `user.achievements` (array of strings) |
| Company | `user.company` | `user.company_name` |
### Where `user.name` is used in the codebase
Search these files for `user.name` and change to `user.full_name` as part of I1:
- `src/components/Layout.jsx` — sidebar user display
- `src/pages/CustomerProfile.jsx` — profile header
- `src/components/Chatbot.jsx` — user greeting
> Other field renames (`empId`, `streak`) are only used in their respective feature pages and will be fixed by the module that integrates those pages (I4, I5).
---
## 8. Session Restore Behaviour
### On every app load / page refresh
```
App mounts
→ AuthProvider mounts
→ isLoading = true
→ GET /auth/me (cookie sent automatically by browser)
→ 200: setUser(data), setIsAuthenticated(true), setIsLoading(false)
→ 401 (expired): interceptors.js fires POST /auth/refresh
→ refresh 200: GET /auth/me retried → user restored
→ refresh 401: catch block runs → setIsAuthenticated(false), setIsLoading(false) → user sees login page
```
### First visit / after logout
```
App mounts → GET /auth/me → 401 (no cookie)
→ interceptors.js does NOT attempt refresh (401 from /auth/me with no cookie)
→ catch block: setUser(null), setIsAuthenticated(false), setIsLoading(false)
→ ProtectedRoute: isLoading=false, isAuthenticated=false → Navigate /login
```
> The interceptor will only retry `/auth/me` if it gets a 401 with a cookie present (access token expired). With no cookie at all, the 401 propagates to the catch block immediately.
---
## 9. I1 Verification Checklist
Before marking I1 complete:
- [ ] `npm install` — axios is in `dependencies`
- [ ] `src/api/auth.js` created with `loginApi`, `getMeApi`, `logoutApi`
- [ ] `src/api/interceptors.js` and `src/api/client.js` exist (from I0)
- [ ] `import './api/interceptors'` present at top of `src/main.jsx`
- [ ] `AuthContext.jsx` no longer imports from `mockStore`
- [ ] `isLoading` exported from `AuthContext.Provider` value
- [ ] `ProtectedRoute` in `App.jsx` has the `isLoading` spinner guard
- [ ] `Login.jsx` `handleLogin` is `async` and `await`s `login()`
- [ ] `Login.jsx` CUSTOMER redirects to `/portal/profile` (not `/`)
- [ ] `Login.jsx` ADMIN redirects to `/admin/dashboard` (not default)
- [ ] Toast on login shows `data.user.full_name` (not `foundUser.name`)
- [ ] Page refresh on a protected route restores session without flashing to `/login`
- [ ] Logout clears cookies (verify in DevTools → Application → Cookies)
- [ ] `fillDemo()` buttons still work (they call `login()` via the form — no changes needed)
---
*Next: Read `02_data_layer_migration.md` (I2) — wrapping `MockStoreProvider` in a feature-flagged `ApiProvider` before any feature module (I3I8) is integrated.*