# 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 (