Hydra-Bolt commited on
Commit
6766784
·
1 Parent(s): 8de5b21
Files changed (3) hide show
  1. NEXTJS_INTEGRATION_GUIDE.md +411 -888
  2. app/api/routes.py +33 -26
  3. app/main.py +1 -1
NEXTJS_INTEGRATION_GUIDE.md CHANGED
@@ -1,960 +1,483 @@
1
- # SanadCheck API Authentication Integration Guide for Next.js
2
-
3
- This comprehensive guide shows how to integrate the SanadCheck LLM API authentication system into a Next.js application. The backend provides JWT-based authentication with Supabase, rate limiting, and comprehensive session management.
4
-
5
- ## Table of Contents
6
- 1. [Backend API Overview](#backend-api-overview)
7
- 2. [Next.js Setup](#nextjs-setup)
8
- 3. [Authentication Service](#authentication-service)
9
- 4. [API Client Setup](#api-client-setup)
10
- 5. [React Hooks for Authentication](#react-hooks-for-authentication)
11
- 6. [Protected Routes & Middleware](#protected-routes--middleware)
12
- 7. [Components](#components)
13
- 8. [Usage Examples](#usage-examples)
14
- 9. [Error Handling](#error-handling)
15
- 10. [Best Practices](#best-practices)
16
-
17
- ## Backend API Overview
18
-
19
- The SanadCheck API provides the following authentication endpoints:
20
-
21
- ### Authentication Endpoints
22
- - `POST /auth/register` - User registration
23
- - `POST /auth/login` - User login
24
- - `POST /auth/refresh` - Token refresh
25
- - `POST /auth/logout` - User logout
26
- - `GET /auth/me` - Get current user info
27
- - `GET /auth/sessions` - Get user sessions
28
-
29
- ### Protected API Endpoints
30
- - `POST /api/v1/extract-narrators` - Extract narrators from hadith
31
- - `POST /api/v1/analyze-narrator` - Analyze individual narrator
32
- - `POST /api/v1/analyze-chain` - Analyze narrator chain
33
- - `POST /api/v1/extract-and-analyze` - Complete analysis
34
-
35
- ### Rate Limiting
36
- - Anonymous users: Limited requests per IP
37
- - Authenticated users: Higher limits per user ID
38
- - Redis-based with burst protection
39
-
40
- ## Next.js Setup
41
-
42
- ### 1. Install Dependencies
43
-
44
- ```bash
45
- npm install axios js-cookie next-auth
46
- # or
47
- yarn add axios js-cookie next-auth
 
 
 
 
 
 
 
48
  ```
49
-
50
- ### 2. Environment Variables
51
-
52
- Create `.env.local`:
53
-
54
- ```env
55
- NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
56
- NEXT_PUBLIC_API_VERSION=v1
57
- NEXTAUTH_SECRET=your-nextauth-secret
58
- NEXTAUTH_URL=http://localhost:3000
59
  ```
60
 
61
- ### 3. Project Structure
62
 
 
 
63
  ```
64
- src/
65
- ├── lib/
66
- │ ├── api.ts # API client configuration
67
- │ ├── auth.ts # Authentication service
68
- │ └── types.ts # TypeScript types
69
- ├── hooks/
70
- │ └── useAuth.ts # Authentication hooks
71
- ├── middleware.ts # Next.js middleware for route protection
72
- ├── components/
73
- │ ├── auth/
74
- │ │ ├── LoginForm.tsx
75
- │ │ ├── RegisterForm.tsx
76
- │ │ └── AuthProvider.tsx
77
- │ └── ui/
78
- │ └── ProtectedRoute.tsx
79
- ├── pages/
80
- │ ├── auth/
81
- │ │ ├── login.tsx
82
- │ │ └── register.tsx
83
- │ ├── dashboard.tsx
84
- │ └── api/
85
- │ └── auth/
86
- │ └── [...nextauth].ts
 
 
 
 
87
  ```
88
 
89
- ## Authentication Service
90
-
91
- ### TypeScript Types (`src/lib/types.ts`)
92
 
93
- ```typescript
 
94
  export interface User {
95
- id: string;
96
- email: string;
97
- username?: string;
98
- full_name?: string;
99
- role: 'user' | 'admin' | 'researcher';
100
- is_active: boolean;
101
- created_at?: string;
102
- last_login?: string;
103
  }
104
 
105
  export interface AuthResponse {
106
- access_token: string;
107
- refresh_token?: string;
108
- token_type: string;
109
- expires_in: number;
110
- user: User;
111
- }
112
-
113
- export interface LoginRequest {
114
- email: string;
115
- password: string;
116
- }
117
-
118
- export interface RegisterRequest {
119
- email: string;
120
- password: string;
121
- username?: string;
122
- full_name?: string;
123
- }
124
-
125
- export interface ApiError {
126
- detail: string;
127
- status_code?: number;
128
- }
129
-
130
- // Hadith Analysis Types
131
- export interface HadithTextRequest {
132
- text: string;
133
- language?: string;
134
  }
135
 
136
  export interface NarratorExtractionResponse {
137
- narrators: string[];
138
- sanad_chain: string;
139
- success: boolean;
140
- metadata: {
141
- processing_time_ms: number;
142
- text_length: number;
143
- extraction_method: string;
144
- };
145
- }
146
-
147
- export interface NarratorAnalysisRequest {
148
- narrator_name: string;
149
- context?: string;
150
  }
151
 
152
  export interface NarratorAnalysisResponse {
153
- narrator: string;
154
- reliability_grade: string;
155
- confidence_level: string;
156
- reasoning: string;
157
- scholarly_consensus: string;
158
- known_issues?: string;
159
- biographical_info: string;
160
- recommendation: string;
161
- success: boolean;
162
- metadata: {
163
- processing_time_ms: number;
164
- analysis_method: string;
165
- };
166
  }
167
  ```
168
 
169
- ### API Client (`src/lib/api.ts`)
170
-
171
- ```typescript
172
- import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
173
- import Cookies from 'js-cookie';
174
-
175
- class ApiClient {
176
- private client: AxiosInstance;
177
- private baseURL: string;
178
-
179
- constructor() {
180
- this.baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
181
-
182
- this.client = axios.create({
183
- baseURL: this.baseURL,
184
- timeout: 30000,
185
- headers: {
186
- 'Content-Type': 'application/json',
187
- },
188
- });
189
-
190
- this.setupInterceptors();
191
- }
192
-
193
- private setupInterceptors() {
194
- // Request interceptor to add auth token
195
- this.client.interceptors.request.use(
196
- (config) => {
197
- const token = Cookies.get('access_token');
198
- if (token) {
199
- config.headers.Authorization = `Bearer ${token}`;
200
- }
201
- return config;
202
- },
203
- (error) => Promise.reject(error)
204
- );
205
-
206
- // Response interceptor for token refresh
207
- this.client.interceptors.response.use(
208
- (response) => response,
209
- async (error) => {
210
- const originalRequest = error.config;
211
-
212
- if (error.response?.status === 401 && !originalRequest._retry) {
213
- originalRequest._retry = true;
214
-
215
- try {
216
- const refreshToken = Cookies.get('refresh_token');
217
- if (refreshToken) {
218
- const response = await this.refreshToken(refreshToken);
219
- const { access_token } = response.data;
220
-
221
- Cookies.set('access_token', access_token, {
222
- expires: 7,
223
- secure: process.env.NODE_ENV === 'production'
224
- });
225
-
226
- originalRequest.headers.Authorization = `Bearer ${access_token}`;
227
- return this.client(originalRequest);
228
- }
229
- } catch (refreshError) {
230
- // Refresh failed, logout user
231
- this.logout();
232
- window.location.href = '/auth/login';
233
- }
234
- }
235
-
236
- return Promise.reject(error);
237
- }
238
- );
239
- }
240
-
241
- // Auth methods
242
- async login(credentials: LoginRequest): Promise<AxiosResponse<AuthResponse>> {
243
- return this.client.post('/auth/login', credentials);
244
- }
245
-
246
- async register(userData: RegisterRequest): Promise<AxiosResponse<AuthResponse>> {
247
- return this.client.post('/auth/register', userData);
248
- }
249
-
250
- async refreshToken(refresh_token: string): Promise<AxiosResponse<AuthResponse>> {
251
- return this.client.post('/auth/refresh', { refresh_token });
252
- }
253
-
254
- async logout(): Promise<void> {
255
- try {
256
- await this.client.post('/auth/logout');
257
- } catch (error) {
258
- console.error('Logout error:', error);
259
- } finally {
260
- Cookies.remove('access_token');
261
- Cookies.remove('refresh_token');
262
- }
263
- }
264
-
265
- async getCurrentUser(): Promise<AxiosResponse<User>> {
266
- return this.client.get('/auth/me');
267
- }
268
-
269
- async getUserSessions(): Promise<AxiosResponse<{ sessions: any[] }>> {
270
- return this.client.get('/auth/sessions');
271
- }
272
-
273
- // Hadith Analysis API methods
274
- async extractNarrators(data: HadithTextRequest): Promise<AxiosResponse<NarratorExtractionResponse>> {
275
- return this.client.post(`/api/${process.env.NEXT_PUBLIC_API_VERSION}/extract-narrators`, data);
276
- }
277
-
278
- async analyzeNarrator(data: NarratorAnalysisRequest): Promise<AxiosResponse<NarratorAnalysisResponse>> {
279
- return this.client.post(`/api/${process.env.NEXT_PUBLIC_API_VERSION}/analyze-narrator`, data);
280
- }
281
-
282
- async analyzeNarratorChain(data: { narrators: string[] }): Promise<AxiosResponse<any>> {
283
- return this.client.post(`/api/${process.env.NEXT_PUBLIC_API_VERSION}/analyze-chain`, data);
284
- }
285
-
286
- async extractAndAnalyze(data: HadithTextRequest): Promise<AxiosResponse<any>> {
287
- return this.client.post(`/api/${process.env.NEXT_PUBLIC_API_VERSION}/extract-and-analyze`, data);
288
- }
289
- }
290
-
291
- export const apiClient = new ApiClient();
292
  ```
293
 
294
- ### Authentication Service (`src/lib/auth.ts`)
295
-
296
- ```typescript
297
- import { apiClient } from './api';
298
- import { User, LoginRequest, RegisterRequest, AuthResponse } from './types';
299
- import Cookies from 'js-cookie';
300
-
301
- export class AuthService {
302
- static async login(credentials: LoginRequest): Promise<{ user: User; tokens: AuthResponse }> {
303
- try {
304
- const response = await apiClient.login(credentials);
305
- const authData = response.data;
306
-
307
- // Store tokens in secure cookies
308
- Cookies.set('access_token', authData.access_token, {
309
- expires: 7,
310
- secure: process.env.NODE_ENV === 'production',
311
- sameSite: 'strict'
312
- });
313
-
314
- if (authData.refresh_token) {
315
- Cookies.set('refresh_token', authData.refresh_token, {
316
- expires: 30,
317
- secure: process.env.NODE_ENV === 'production',
318
- sameSite: 'strict'
319
- });
320
- }
321
-
322
- return {
323
- user: authData.user,
324
- tokens: authData
325
- };
326
- } catch (error: any) {
327
- throw new Error(error.response?.data?.detail || 'Login failed');
328
- }
329
- }
330
-
331
- static async register(userData: RegisterRequest): Promise<{ user: User; tokens: AuthResponse }> {
332
- try {
333
- const response = await apiClient.register(userData);
334
- const authData = response.data;
335
-
336
- // Store tokens in secure cookies
337
- Cookies.set('access_token', authData.access_token, {
338
- expires: 7,
339
- secure: process.env.NODE_ENV === 'production',
340
- sameSite: 'strict'
341
- });
342
-
343
- if (authData.refresh_token) {
344
- Cookies.set('refresh_token', authData.refresh_token, {
345
- expires: 30,
346
- secure: process.env.NODE_ENV === 'production',
347
- sameSite: 'strict'
348
- });
349
- }
350
-
351
- return {
352
- user: authData.user,
353
- tokens: authData
354
- };
355
- } catch (error: any) {
356
- throw new Error(error.response?.data?.detail || 'Registration failed');
357
- }
358
- }
359
-
360
- static async logout(): Promise<void> {
361
- await apiClient.logout();
362
- }
363
-
364
- static async getCurrentUser(): Promise<User | null> {
365
- try {
366
- const token = Cookies.get('access_token');
367
- if (!token) return null;
368
-
369
- const response = await apiClient.getCurrentUser();
370
- return response.data;
371
- } catch (error) {
372
- return null;
373
- }
374
- }
375
-
376
- static isAuthenticated(): boolean {
377
- return !!Cookies.get('access_token');
378
- }
379
-
380
- static getToken(): string | undefined {
381
- return Cookies.get('access_token');
382
- }
383
- }
384
- ```
385
 
386
- ## React Hooks for Authentication
 
 
387
 
388
- ### Authentication Hook (`src/hooks/useAuth.ts`)
389
 
390
- ```typescript
391
- 'use client';
 
 
392
 
393
- import { useState, useEffect, useContext, createContext, ReactNode } from 'react';
394
- import { User, LoginRequest, RegisterRequest } from '@/lib/types';
395
- import { AuthService } from '@/lib/auth';
396
-
397
- interface AuthContextType {
398
- user: User | null;
399
- loading: boolean;
400
- error: string | null;
401
- login: (credentials: LoginRequest) => Promise<void>;
402
- register: (userData: RegisterRequest) => Promise<void>;
403
- logout: () => Promise<void>;
404
- clearError: () => void;
405
- }
406
 
407
- const AuthContext = createContext<AuthContextType | undefined>(undefined);
408
-
409
- export function AuthProvider({ children }: { children: ReactNode }) {
410
- const [user, setUser] = useState<User | null>(null);
411
- const [loading, setLoading] = useState(true);
412
- const [error, setError] = useState<string | null>(null);
413
-
414
- useEffect(() => {
415
- loadUser();
416
- }, []);
417
-
418
- const loadUser = async () => {
419
- try {
420
- setLoading(true);
421
- const currentUser = await AuthService.getCurrentUser();
422
- setUser(currentUser);
423
- } catch (error) {
424
- console.error('Failed to load user:', error);
425
- } finally {
426
- setLoading(false);
427
- }
428
- };
429
-
430
- const login = async (credentials: LoginRequest) => {
431
- try {
432
- setLoading(true);
433
- setError(null);
434
- const { user } = await AuthService.login(credentials);
435
- setUser(user);
436
- } catch (error: any) {
437
- setError(error.message);
438
- throw error;
439
- } finally {
440
- setLoading(false);
441
- }
442
- };
443
-
444
- const register = async (userData: RegisterRequest) => {
445
- try {
446
- setLoading(true);
447
- setError(null);
448
- const { user } = await AuthService.register(userData);
449
- setUser(user);
450
- } catch (error: any) {
451
- setError(error.message);
452
- throw error;
453
- } finally {
454
- setLoading(false);
455
- }
456
- };
457
-
458
- const logout = async () => {
459
- try {
460
- setLoading(true);
461
- await AuthService.logout();
462
- setUser(null);
463
- } catch (error) {
464
- console.error('Logout error:', error);
465
- } finally {
466
- setLoading(false);
467
- }
468
- };
469
-
470
- const clearError = () => setError(null);
471
-
472
- return (
473
- <AuthContext.Provider
474
- value={{
475
- user,
476
- loading,
477
- error,
478
- login,
479
- register,
480
- logout,
481
- clearError,
482
- }}
483
- >
484
- {children}
485
- </AuthContext.Provider>
486
- );
487
  }
488
 
489
- export function useAuth() {
490
- const context = useContext(AuthContext);
491
- if (context === undefined) {
492
- throw new Error('useAuth must be used within an AuthProvider');
493
- }
494
- return context;
 
 
 
 
 
 
 
 
 
 
 
495
  }
496
  ```
497
 
498
- ## Protected Routes & Middleware
499
-
500
- ### Next.js Middleware (`src/middleware.ts`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- ```typescript
 
 
503
  import { NextResponse } from 'next/server';
504
- import type { NextRequest } from 'next/server';
505
-
506
- export function middleware(request: NextRequest) {
507
- const token = request.cookies.get('access_token')?.value;
508
- const { pathname } = request.nextUrl;
509
-
510
- // Define protected routes
511
- const protectedRoutes = ['/dashboard', '/profile', '/analysis'];
512
- const authRoutes = ['/auth/login', '/auth/register'];
513
-
514
- // Check if the current route is protected
515
- const isProtectedRoute = protectedRoutes.some(route =>
516
- pathname.startsWith(route)
517
- );
518
 
519
- // Check if the current route is an auth route
520
- const isAuthRoute = authRoutes.some(route =>
521
- pathname.startsWith(route)
522
- );
523
-
524
- // Redirect to login if accessing protected route without token
525
- if (isProtectedRoute && !token) {
526
- return NextResponse.redirect(new URL('/auth/login', request.url));
527
- }
528
-
529
- // Redirect to dashboard if accessing auth routes while logged in
530
- if (isAuthRoute && token) {
531
- return NextResponse.redirect(new URL('/dashboard', request.url));
532
- }
533
-
534
- return NextResponse.next();
535
  }
 
536
 
537
- export const config = {
538
- matcher: [
539
- '/((?!api|_next/static|_next/image|favicon.ico).*)',
540
- ],
541
- };
 
 
 
 
 
 
 
 
 
542
  ```
543
 
544
- ### Protected Route Component (`src/components/ui/ProtectedRoute.tsx`)
545
 
546
- ```typescript
547
- 'use client';
548
 
549
- import { useAuth } from '@/hooks/useAuth';
550
- import { useRouter } from 'next/navigation';
551
- import { useEffect, ReactNode } from 'react';
552
 
553
- interface ProtectedRouteProps {
554
- children: ReactNode;
555
- requiredRole?: 'user' | 'admin' | 'researcher';
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  }
 
557
 
558
- export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
559
- const { user, loading } = useAuth();
560
- const router = useRouter();
561
-
562
- useEffect(() => {
563
- if (!loading) {
564
- if (!user) {
565
- router.push('/auth/login');
566
- return;
567
- }
568
-
569
- if (requiredRole && user.role !== requiredRole && user.role !== 'admin') {
570
- router.push('/unauthorized');
571
- return;
572
- }
573
- }
574
- }, [user, loading, router, requiredRole]);
575
-
576
- if (loading) {
577
- return (
578
- <div className="flex items-center justify-center min-h-screen">
579
- <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
580
- </div>
581
- );
582
- }
583
-
584
- if (!user) {
585
- return null;
586
- }
587
-
588
- if (requiredRole && user.role !== requiredRole && user.role !== 'admin') {
589
- return null;
590
- }
591
-
592
- return <>{children}</>;
593
  }
594
  ```
595
 
596
- ## Components
 
597
 
598
- ### Login Form (`src/components/auth/LoginForm.tsx`)
599
-
600
- ```typescript
601
- 'use client';
602
-
603
- import { useState } from 'react';
604
- import { useAuth } from '@/hooks/useAuth';
605
- import { useRouter } from 'next/navigation';
606
- import Link from 'next/link';
607
 
608
- export function LoginForm() {
609
- const [email, setEmail] = useState('');
610
- const [password, setPassword] = useState('');
611
- const { login, loading, error, clearError } = useAuth();
612
- const router = useRouter();
613
-
614
- const handleSubmit = async (e: React.FormEvent) => {
615
- e.preventDefault();
616
- clearError();
617
-
618
- try {
619
- await login({ email, password });
620
- router.push('/dashboard');
621
- } catch (error) {
622
- // Error is handled by the useAuth hook
623
- }
624
- };
625
-
626
- return (
627
- <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
628
- <h2 className="text-2xl font-bold mb-6 text-center">Login to SanadCheck</h2>
629
-
630
- {error && (
631
- <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
632
- {error}
633
- </div>
634
- )}
635
-
636
- <form onSubmit={handleSubmit} className="space-y-4">
637
- <div>
638
- <label htmlFor="email" className="block text-sm font-medium text-gray-700">
639
- Email
640
- </label>
641
- <input
642
- type="email"
643
- id="email"
644
- value={email}
645
- onChange={(e) => setEmail(e.target.value)}
646
- required
647
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
648
- />
649
- </div>
650
-
651
- <div>
652
- <label htmlFor="password" className="block text-sm font-medium text-gray-700">
653
- Password
654
- </label>
655
- <input
656
- type="password"
657
- id="password"
658
- value={password}
659
- onChange={(e) => setPassword(e.target.value)}
660
- required
661
- minLength={6}
662
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
663
- />
664
- </div>
665
-
666
- <button
667
- type="submit"
668
- disabled={loading}
669
- className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
670
- >
671
- {loading ? 'Signing in...' : 'Sign in'}
672
- </button>
673
- </form>
674
-
675
- <p className="mt-4 text-center text-sm text-gray-600">
676
- Don't have an account?{' '}
677
- <Link href="/auth/register" className="font-medium text-indigo-600 hover:text-indigo-500">
678
- Sign up
679
- </Link>
680
- </p>
681
- </div>
682
- );
683
  }
684
  ```
 
685
 
686
- ### Hadith Analysis Component (`src/components/analysis/HadithAnalyzer.tsx`)
687
-
688
- ```typescript
689
  'use client';
690
-
691
  import { useState } from 'react';
692
- import { apiClient } from '@/lib/api';
693
- import { HadithTextRequest, NarratorExtractionResponse } from '@/lib/types';
694
-
695
- export function HadithAnalyzer() {
696
- const [text, setText] = useState('');
697
- const [loading, setLoading] = useState(false);
698
- const [result, setResult] = useState<NarratorExtractionResponse | null>(null);
699
- const [error, setError] = useState<string | null>(null);
700
-
701
- const handleAnalyze = async (e: React.FormEvent) => {
702
- e.preventDefault();
703
- if (!text.trim()) return;
704
-
705
- setLoading(true);
706
- setError(null);
707
-
708
- try {
709
- const response = await apiClient.extractNarrators({ text });
710
- setResult(response.data);
711
- } catch (err: any) {
712
- setError(err.response?.data?.detail || 'Analysis failed');
713
- } finally {
714
- setLoading(false);
715
- }
716
- };
717
-
718
- return (
719
- <div className="max-w-4xl mx-auto p-6">
720
- <h2 className="text-2xl font-bold mb-6">Hadith Narrator Analysis</h2>
721
-
722
- <form onSubmit={handleAnalyze} className="mb-8">
723
- <div>
724
- <label htmlFor="hadith-text" className="block text-sm font-medium text-gray-700 mb-2">
725
- Enter Hadith Text (Arabic)
726
- </label>
727
- <textarea
728
- id="hadith-text"
729
- value={text}
730
- onChange={(e) => setText(e.target.value)}
731
- rows={6}
732
- className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
733
- placeholder="أخبرنا..."
734
- required
735
- />
736
- </div>
737
-
738
- <button
739
- type="submit"
740
- disabled={loading || !text.trim()}
741
- className="mt-4 px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
742
- >
743
- {loading ? 'Analyzing...' : 'Analyze Narrators'}
744
- </button>
745
- </form>
746
-
747
- {error && (
748
- <div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
749
- Error: {error}
750
- </div>
751
- )}
752
-
753
- {result && (
754
- <div className="bg-white rounded-lg shadow-md p-6">
755
- <h3 className="text-lg font-semibold mb-4">Analysis Results</h3>
756
-
757
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
758
- <div>
759
- <h4 className="font-medium text-gray-900 mb-2">Extracted Narrators</h4>
760
- <ul className="space-y-1">
761
- {result.narrators.map((narrator, index) => (
762
- <li key={index} className="text-gray-700 bg-gray-50 px-3 py-1 rounded">
763
- {narrator}
764
- </li>
765
- ))}
766
- </ul>
767
- </div>
768
-
769
- <div>
770
- <h4 className="font-medium text-gray-900 mb-2">Sanad Chain</h4>
771
- <p className="text-gray-700 bg-gray-50 p-3 rounded">
772
- {result.sanad_chain}
773
- </p>
774
- </div>
775
- </div>
776
-
777
- <div className="mt-4 text-sm text-gray-500">
778
- Processing time: {result.metadata.processing_time_ms}ms
779
- </div>
780
- </div>
781
- )}
782
- </div>
783
- );
784
  }
785
  ```
786
 
787
- ## Usage Examples
788
-
789
- ### App Layout with Authentication (`src/app/layout.tsx`)
790
-
791
- ```typescript
792
- import { AuthProvider } from '@/hooks/useAuth';
793
- import type { Metadata } from 'next';
794
-
795
- export const metadata: Metadata = {
796
- title: 'SanadCheck - Hadith Narrator Analysis',
797
- description: 'AI-powered hadith narrator analysis and validation',
798
- };
799
-
800
- export default function RootLayout({
801
- children,
802
- }: {
803
- children: React.ReactNode;
804
- }) {
805
- return (
806
- <html lang="en">
807
- <body>
808
- <AuthProvider>
809
- {children}
810
- </AuthProvider>
811
- </body>
812
- </html>
813
- );
814
  }
815
  ```
 
816
 
817
- ### Dashboard Page (`src/app/dashboard/page.tsx`)
818
-
819
- ```typescript
820
- 'use client';
 
 
821
 
822
- import { ProtectedRoute } from '@/components/ui/ProtectedRoute';
823
- import { HadithAnalyzer } from '@/components/analysis/HadithAnalyzer';
824
- import { useAuth } from '@/hooks/useAuth';
825
-
826
- export default function Dashboard() {
827
- const { user, logout } = useAuth();
828
-
829
- return (
830
- <ProtectedRoute>
831
- <div className="min-h-screen bg-gray-50">
832
- <nav className="bg-white shadow-sm border-b">
833
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
834
- <div className="flex justify-between h-16">
835
- <div className="flex items-center">
836
- <h1 className="text-xl font-semibold">SanadCheck Dashboard</h1>
837
- </div>
838
- <div className="flex items-center space-x-4">
839
- <span className="text-gray-700">Welcome, {user?.username || user?.email}</span>
840
- <button
841
- onClick={logout}
842
- className="text-gray-500 hover:text-gray-700"
843
- >
844
- Logout
845
- </button>
846
- </div>
847
- </div>
848
- </div>
849
- </nav>
850
-
851
- <main className="py-8">
852
- <HadithAnalyzer />
853
- </main>
854
- </div>
855
- </ProtectedRoute>
856
- );
857
  }
858
  ```
 
 
 
 
859
 
860
- ## Error Handling
861
-
862
- ### Global Error Handler (`src/lib/errorHandler.ts`)
863
-
864
- ```typescript
865
- import { AxiosError } from 'axios';
866
-
867
- export interface ApiErrorResponse {
868
- detail: string;
869
- status_code?: number;
 
 
 
 
 
 
870
  }
871
 
872
- export class AppError extends Error {
873
- public statusCode: number;
874
- public isOperational: boolean;
875
-
876
- constructor(message: string, statusCode = 500, isOperational = true) {
877
- super(message);
878
- this.statusCode = statusCode;
879
- this.isOperational = isOperational;
880
-
881
- Error.captureStackTrace(this, this.constructor);
882
- }
883
- }
884
 
885
- export function handleApiError(error: AxiosError<ApiErrorResponse>): AppError {
886
- const message = error.response?.data?.detail || error.message || 'An unexpected error occurred';
887
- const statusCode = error.response?.status || 500;
888
-
889
- switch (statusCode) {
890
- case 401:
891
- return new AppError('Authentication required', 401);
892
- case 403:
893
- return new AppError('Access denied', 403);
894
- case 404:
895
- return new AppError('Resource not found', 404);
896
- case 429:
897
- return new AppError('Rate limit exceeded. Please try again later.', 429);
898
- case 500:
899
- return new AppError('Server error. Please try again later.', 500);
900
- default:
901
- return new AppError(message, statusCode);
902
- }
903
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  ```
905
 
906
- ## Best Practices
907
-
908
- ### 1. Security
909
- - Store tokens in secure, httpOnly cookies when possible
910
- - Use HTTPS in production
911
- - Implement proper CORS policies
912
- - Validate all inputs on both client and server
913
- - Use environment variables for sensitive data
914
-
915
- ### 2. Performance
916
- - Implement proper loading states
917
- - Use React.memo for expensive components
918
- - Debounce API calls for search functionality
919
- - Cache user data appropriately
920
-
921
- ### 3. Error Handling
922
- - Provide meaningful error messages
923
- - Implement retry logic for network failures
924
- - Handle rate limiting gracefully
925
- - Log errors for debugging
926
-
927
- ### 4. User Experience
928
- - Show loading indicators
929
- - Implement optimistic updates where appropriate
930
- - Provide clear feedback for all actions
931
- - Handle offline scenarios
932
-
933
- ### 5. Code Organization
934
- - Separate API logic from UI components
935
- - Use TypeScript for type safety
936
- - Follow consistent naming conventions
937
- - Implement proper error boundaries
938
-
939
- ### 6. Testing
940
- ```typescript
941
- // Example test for authentication hook
942
- import { renderHook, act } from '@testing-library/react';
943
- import { useAuth } from '@/hooks/useAuth';
944
-
945
- test('should login user successfully', async () => {
946
- const { result } = renderHook(() => useAuth());
947
-
948
- await act(async () => {
949
- await result.current.login({
950
- email: 'test@example.com',
951
- password: 'password123'
952
- });
953
- });
954
-
955
- expect(result.current.user).toBeTruthy();
956
- expect(result.current.error).toBeNull();
957
- });
958
  ```
959
 
960
- This comprehensive guide provides everything needed to integrate the SanadCheck API authentication system into a Next.js application. The implementation includes proper error handling, security practices, and a scalable architecture for hadith analysis features.
 
 
 
 
 
 
 
1
+ ## SanadCheck LLM API Next.js Integration (with Authentication)
2
+
3
+ This guide shows how to securely integrate the SanadCheck API into a Next.js 13/14+ (App Router) project, with proper JWT handling (access + refresh), protected server actions, and client usage examples.
4
+
5
+ ---
6
+ ## 1. High-Level Architecture
7
+
8
+ Recommended pattern: keep ALL direct calls to the FastAPI service on the server (Next.js Route Handlers / Server Actions) so refresh tokens never reach the browser's JS runtime.
9
+
10
+ Flow:
11
+ 1. User submits credentials (email/password) via a client component form.
12
+ 2. Form calls a Next.js Route Handler: `POST /api/auth/login`.
13
+ 3. Route handler forwards to FastAPI `/auth/login`.
14
+ 4. Response contains: `access_token`, `refresh_token`, `expires_in`, `user`.
15
+ 5. Next handler sets:
16
+ - `sc_refresh` (HttpOnly, Secure, SameSite=Strict) – refresh token
17
+ - `sc_access` (HttpOnly OR short-lived in-memory re-fetched via server action) – access token
18
+ - Optionally store decoded `user` object in a signed cookie or re-fetch via `/auth/me` per request.
19
+ 6. Client components fetch data by calling internal Next.js API routes (proxy) that attach `Authorization: Bearer <access_token>`.
20
+ 7. If access token expired, the proxy handler uses the refresh token cookie to get a new pair (rotate!) transparently.
21
+
22
+ Why this pattern:
23
+ - Avoids XSS exposure of refresh tokens
24
+ - Centralizes refresh logic
25
+ - Enables SSR + Server Actions with authenticated context
26
+
27
+ ---
28
+ ## 2. Backend Endpoints Used
29
+
30
+ Auth:
31
+ - `POST /auth/register`
32
+ - `POST /auth/login`
33
+ - `POST /auth/refresh`
34
+ - `POST /auth/logout`
35
+ - `GET /auth/me`
36
+ - `GET /auth/sessions`
37
+
38
+ Core Hadith Analysis (all protected except `/api/v1/health` + some analytics):
39
+ - `POST /api/v1/extract-narrators`
40
+ - `POST /api/v1/analyze-narrator`
41
+ - `POST /api/v1/analyze-narrator-chain`
42
+ - `POST /api/v1/extract-and-analyze`
43
+ - `GET /api/v1/user/extractions`
44
+ - `GET /api/v1/user/analyses`
45
+ - `GET /api/v1/analytics/stats` (public)
46
+ - `GET /api/v1/analytics/popular-narrators` (public)
47
+ - `GET /api/v1/health` (public)
48
+
49
+ Rate limit errors will return 429 and headers: `X-RateLimit-*`.
50
+
51
+ ---
52
+ ## 3. Environment Variables (Next.js)
53
+
54
+ In `.env.local`:
55
  ```
56
+ SANAD_API_BASE_URL=http://localhost:8000 # FastAPI base
57
+ NEXT_PUBLIC_SANAD_PUBLIC_BASE=/api/sanad # Public-facing proxy base (optional)
 
 
 
 
 
 
 
 
58
  ```
59
 
60
+ Never expose service keys. Only the public base URL may be exposed.
61
 
62
+ ---
63
+ ## 4. Directory Structure (Suggested)
64
  ```
65
+ app/
66
+ api/
67
+ auth/
68
+ login/route.ts
69
+ register/route.ts
70
+ logout/route.ts
71
+ refresh/route.ts # (Optional explicit refresh)
72
+ me/route.ts
73
+ sanad/
74
+ extract-narrators/route.ts
75
+ analyze-narrator/route.ts
76
+ extract-and-analyze/route.ts
77
+ user/
78
+ extractions/route.ts
79
+ analyses/route.ts
80
+ analytics/
81
+ stats/route.ts
82
+ popular-narrators/route.ts
83
+ (UI pages & components...)
84
+ lib/
85
+ apiClient.ts
86
+ auth.ts
87
+ tokens.ts
88
+ middleware.ts (optional token freshness logic)
89
+ components/
90
+ AuthProvider.tsx
91
+ LoginForm.tsx
92
  ```
93
 
94
+ ---
95
+ ## 5. TypeScript Models
 
96
 
97
+ Create `lib/types.ts`:
98
+ ```ts
99
  export interface User {
100
+ id: string;
101
+ email: string;
102
+ username: string;
103
+ full_name: string;
104
+ role: string;
105
+ is_active: boolean;
 
 
106
  }
107
 
108
  export interface AuthResponse {
109
+ access_token: string;
110
+ refresh_token: string;
111
+ token_type: 'bearer';
112
+ expires_in: number; // seconds
113
+ user: User;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
  export interface NarratorExtractionResponse {
117
+ narrators: string[];
118
+ sanad_chain: string;
119
+ success: boolean;
120
+ message?: string;
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
  export interface NarratorAnalysisResponse {
124
+ narrator_name: string;
125
+ reliability_grade: string;
126
+ confidence_level: string;
127
+ reasoning: string;
128
+ scholarly_consensus: string;
129
+ known_issues?: string[] | null;
130
+ biographical_info: string;
131
+ recommendation: string;
132
+ success: boolean;
133
+ message?: string;
 
 
 
134
  }
135
  ```
136
 
137
+ ---
138
+ ## 6. Secure Token Handling Strategy
139
+
140
+ | Aspect | Recommendation |
141
+ |--------|---------------|
142
+ | Access Token | Short-lived (minutes). Store in httpOnly cookie `sc_access` or re-fetch via server action each request. |
143
+ | Refresh Token | HttpOnly + Secure cookie `sc_refresh` only. Never expose to JS. |
144
+ | Rotation | Always replace both tokens on refresh (server enforces blacklisting). |
145
+ | Expiry Tracking | Store `exp` in cookie or decode server-side. Trigger refresh when <60s left. |
146
+ | Logout | Clear both cookies + call backend `/auth/logout` with current access token. |
147
+
148
+ Cookie examples (set in route handlers):
149
+ ```ts
150
+ cookies().set('sc_refresh', refreshToken, {
151
+ httpOnly: true,
152
+ secure: process.env.NODE_ENV === 'production',
153
+ sameSite: 'strict',
154
+ path: '/',
155
+ maxAge: 60 * 60 * 24 * 7, // adjust to backend refresh expiry
156
+ });
157
+ cookies().set('sc_access', accessToken, {
158
+ httpOnly: true,
159
+ secure: process.env.NODE_ENV === 'production',
160
+ sameSite: 'strict',
161
+ path: '/',
162
+ maxAge: 60 * 15,
163
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  ```
165
 
166
+ ---
167
+ ## 7. Generic Fetch Wrapper (Server)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ `lib/apiClient.ts`:
170
+ ```ts
171
+ import { cookies } from 'next/headers';
172
 
173
+ const BASE = process.env.SANAD_API_BASE_URL!;
174
 
175
+ async function refreshIfNeeded(): Promise<string | null> {
176
+ const store = cookies();
177
+ const access = store.get('sc_access')?.value;
178
+ if (access) return access; // Or decode & check exp
179
 
180
+ const refresh = store.get('sc_refresh')?.value;
181
+ if (!refresh) return null;
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ // Call internal refresh route (not FastAPI directly)
184
+ const res = await fetch(`${process.env.NEXT_PUBLIC_SANAD_PUBLIC_BASE || ''}/auth/refresh`, { method: 'POST' });
185
+ if (!res.ok) return null;
186
+ const data = await res.json();
187
+ return cookies().get('sc_access')?.value || null; // After handler sets new cookie
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
 
190
+ export async function sanadFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
191
+ const token = await refreshIfNeeded();
192
+ const headers = new Headers(init.headers || {});
193
+ if (token) headers.set('Authorization', `Bearer ${token}`);
194
+ headers.set('Content-Type', 'application/json');
195
+
196
+ const res = await fetch(`${BASE}${path}`, { ...init, headers, cache: 'no-store' });
197
+ if (res.status === 429) {
198
+ const limit = res.headers.get('X-RateLimit-Limit');
199
+ const reset = res.headers.get('X-RateLimit-Reset');
200
+ throw new Error(`Rate limited. Limit=${limit} resets at=${reset}`);
201
+ }
202
+ if (!res.ok) {
203
+ const body = await res.text();
204
+ throw new Error(`Sanad API error ${res.status}: ${body}`);
205
+ }
206
+ return res.json() as Promise<T>;
207
  }
208
  ```
209
 
210
+ ---
211
+ ## 8. Route Handler Examples (Proxy Pattern)
212
+
213
+ `app/api/auth/login/route.ts`:
214
+ ```ts
215
+ import { NextRequest, NextResponse } from 'next/server';
216
+ import { cookies } from 'next/headers';
217
+
218
+ export async function POST(req: NextRequest) {
219
+ const creds = await req.json();
220
+ const res = await fetch(`${process.env.SANAD_API_BASE_URL}/auth/login`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify(creds),
224
+ });
225
+ if (!res.ok) {
226
+ return NextResponse.json({ error: 'Login failed' }, { status: res.status });
227
+ }
228
+ const data = await res.json();
229
+ const c = cookies();
230
+ c.set('sc_refresh', data.refresh_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: data.expires_in * 4 });
231
+ c.set('sc_access', data.access_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: data.expires_in });
232
+ return NextResponse.json({ user: data.user });
233
+ }
234
+ ```
235
 
236
+ `app/api/auth/refresh/route.ts`:
237
+ ```ts
238
+ import { cookies } from 'next/headers';
239
  import { NextResponse } from 'next/server';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ export async function POST() {
242
+ const refresh = cookies().get('sc_refresh')?.value;
243
+ if (!refresh) return NextResponse.json({ error: 'No refresh token' }, { status: 401 });
244
+
245
+ const res = await fetch(`${process.env.SANAD_API_BASE_URL}/auth/refresh`, {
246
+ method: 'POST',
247
+ headers: { 'Content-Type': 'application/json' },
248
+ body: JSON.stringify({ refresh_token: refresh }),
249
+ });
250
+ if (!res.ok) return NextResponse.json({ error: 'Refresh failed' }, { status: 401 });
251
+ const data = await res.json();
252
+ const c = cookies();
253
+ c.set('sc_refresh', data.refresh_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: data.expires_in * 4 });
254
+ c.set('sc_access', data.access_token, { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: data.expires_in });
255
+ return NextResponse.json({ status: 'refreshed' });
 
256
  }
257
+ ```
258
 
259
+ `app/api/sanad/extract-narrators/route.ts`:
260
+ ```ts
261
+ import { NextRequest, NextResponse } from 'next/server';
262
+ import { sanadFetch } from '@/lib/apiClient';
263
+
264
+ export async function POST(req: NextRequest) {
265
+ const body = await req.json();
266
+ try {
267
+ const data = await sanadFetch('/api/v1/extract-narrators', { method: 'POST', body: JSON.stringify(body) });
268
+ return NextResponse.json(data);
269
+ } catch (e: any) {
270
+ return NextResponse.json({ error: e.message }, { status: 500 });
271
+ }
272
+ }
273
  ```
274
 
275
+ Add similar handlers for `analyze-narrator`, `extract-and-analyze`, etc.
276
 
277
+ ---
278
+ ## 9. Client Hook (Optional Thin Layer)
279
 
280
+ `lib/useSanad.ts` (Client Component safe):
281
+ ```ts
282
+ import { useState } from 'react';
283
 
284
+ export function useSanad() {
285
+ const [loading, setLoading] = useState(false);
286
+ const [error, setError] = useState<string | null>(null);
287
+
288
+ async function post<T>(path: string, payload: any): Promise<T | null> {
289
+ setLoading(true); setError(null);
290
+ try {
291
+ const res = await fetch(`/api/sanad${path}`, { method: 'POST', body: JSON.stringify(payload) });
292
+ const json = await res.json();
293
+ if (!res.ok) throw new Error(json.error || 'Request failed');
294
+ return json as T;
295
+ } catch (e: any) { setError(e.message); return null; }
296
+ finally { setLoading(false); }
297
+ }
298
+
299
+ return { post, loading, error };
300
  }
301
+ ```
302
 
303
+ Usage in a component:
304
+ ```tsx
305
+ const { post, loading, error } = useSanad();
306
+ async function handleExtract(text: string) {
307
+ const data = await post('/extract-narrators', { hadith_text: text });
308
+ console.log(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
  ```
311
 
312
+ ---
313
+ ## 10. Server Action Example (Optional)
314
 
315
+ If you prefer server actions instead of route handlers for some flows:
316
+ ```ts
317
+ 'use server';
318
+ import { sanadFetch } from '@/lib/apiClient';
 
 
 
 
 
319
 
320
+ export async function extractNarratorsAction(hadith: string) {
321
+ return sanadFetch('/api/v1/extract-narrators', { method: 'POST', body: JSON.stringify({ hadith_text: hadith }) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
  ```
324
+ Call inside a Server Component or via form action.
325
 
326
+ ---
327
+ ## 11. Login Form (Client) Example
328
+ ```tsx
329
  'use client';
 
330
  import { useState } from 'react';
331
+
332
+ export function LoginForm() {
333
+ const [email, setEmail] = useState('');
334
+ const [password, setPassword] = useState('');
335
+ const [error, setError] = useState<string | null>(null);
336
+
337
+ async function submit(e: React.FormEvent) {
338
+ e.preventDefault(); setError(null);
339
+ const res = await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) });
340
+ const data = await res.json();
341
+ if (!res.ok) setError(data.error || 'Login failed');
342
+ // success: user available in data.user, tokens stored as cookies
343
+ }
344
+
345
+ return (
346
+ <form onSubmit={submit} className="space-y-2">
347
+ <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
348
+ <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" />
349
+ <button type="submit">Login</button>
350
+ {error && <p className="text-red-500">{error}</p>}
351
+ </form>
352
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  }
354
  ```
355
 
356
+ ---
357
+ ## 12. Handling Rate Limits & Errors
358
+
359
+ Pattern:
360
+ ```ts
361
+ try {
362
+ const res = await sanadFetch('/api/v1/analyze-narrator', { method: 'POST', body: JSON.stringify({ narrator_name }) });
363
+ } catch (e: any) {
364
+ if (e.message.includes('429')) {
365
+ // show friendly message / retry after header
366
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
  ```
369
+ Consider exponential backoff for bursts and surface `X-RateLimit-Remaining` to show usage.
370
 
371
+ ---
372
+ ## 13. Logout Flow
373
+ Route handler `app/api/auth/logout/route.ts`:
374
+ ```ts
375
+ import { cookies } from 'next/headers';
376
+ import { NextResponse } from 'next/server';
377
 
378
+ export async function POST() {
379
+ const access = cookies().get('sc_access')?.value;
380
+ if (access) {
381
+ await fetch(`${process.env.SANAD_API_BASE_URL}/auth/logout`, {
382
+ method: 'POST',
383
+ headers: { Authorization: `Bearer ${access}` }
384
+ }).catch(() => {});
385
+ }
386
+ const c = cookies();
387
+ c.delete('sc_access');
388
+ c.delete('sc_refresh');
389
+ return NextResponse.json({ status: 'logged_out' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
  ```
392
+ Client call:
393
+ ```ts
394
+ await fetch('/api/auth/logout', { method: 'POST' });
395
+ ```
396
 
397
+ ---
398
+ ## 14. Middleware (Optional Early Refresh)
399
+
400
+ `middleware.ts` (basic sketch – only if you want silent refresh before protected routes):
401
+ ```ts
402
+ import { NextRequest, NextResponse } from 'next/server';
403
+
404
+ export async function middleware(req: NextRequest) {
405
+ const access = req.cookies.get('sc_access');
406
+ if (!access) {
407
+ const refresh = req.cookies.get('sc_refresh');
408
+ if (refresh) {
409
+ await fetch(new URL('/api/auth/refresh', req.url), { method: 'POST' });
410
+ }
411
+ }
412
+ return NextResponse.next();
413
  }
414
 
415
+ export const config = { matcher: ['/dashboard/:path*', '/analysis/:path*'] };
416
+ ```
 
 
 
 
 
 
 
 
 
 
417
 
418
+ ---
419
+ ## 15. Security Checklist
420
+
421
+ - Use `Secure` cookies in production (HTTPS)
422
+ - Set `SameSite=Strict` to mitigate CSRF (or use custom CSRF token if cross-site embedding needed)
423
+ - Do NOT store refresh tokens in `localStorage` or JS-accessible cookies
424
+ - Rotate tokens on every refresh
425
+ - Handle logout on 401 loops (e.g., if refresh also fails)
426
+
427
+ ---
428
+ ## 16. Minimal End-to-End Example
429
+
430
+ 1. User visits `/login` submits form
431
+ 2. `/api/auth/login` sets cookies
432
+ 3. User navigates to `/dashboard` (protected)
433
+ 4. Server component calls server action or `fetch('/api/sanad/extract-narrators')`
434
+ 5. Handler attaches Authorization header → FastAPI processes → returns JSON
435
+ 6. Access token expires → next call triggers `/api/auth/refresh` implicitly → cookies updated
436
+ 7. User clicks logout → `/api/auth/logout` → cookies cleared + token blacklisted
437
+
438
+ ---
439
+ ## 17. Troubleshooting
440
+
441
+ | Symptom | Cause | Fix |
442
+ |---------|-------|-----|
443
+ | 401 on every request | Missing Authorization header | Ensure proxy sets header after refresh |
444
+ | 401 after refresh | Refresh token expired/blacklisted | Force logout & re-login |
445
+ | 429 errors | Rate limit exceeded | Slow down / show user retry time |
446
+ | CORS errors (if bypassing proxy) | Direct browser → FastAPI without proper CORS | Always use Next.js proxy or configure CORS on backend |
447
+ | Cookie not set in prod | Missing `secure` or domain mismatch | Set correct domain & HTTPS |
448
+
449
+ ---
450
+ ## 18. Next Steps / Enhancements
451
+
452
+ - Add SWR/React Query for caching
453
+ - Add optimistic UI for chain analysis
454
+ - Add user session list page using `/auth/sessions`
455
+ - Implement progress indicators for long analyses
456
+
457
+ ---
458
+ ## 19. Quick Reference (Cheat Sheet)
459
+
460
+ Auth Cycle:
461
+ ```
462
+ POST /api/auth/login -> sets cookies
463
+ POST /api/auth/refresh -> rotates tokens
464
+ POST /api/auth/logout -> clears + blacklists
465
+ GET /api/auth/me -> user profile
466
  ```
467
 
468
+ Core Calls:
469
+ ```
470
+ POST /api/sanad/extract-narrators
471
+ POST /api/sanad/analyze-narrator
472
+ POST /api/sanad/extract-and-analyze
473
+ GET /api/sanad/user/extractions
474
+ GET /api/sanad/user/analyses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  ```
476
 
477
+ ---
478
+ ## 20. Summary
479
+
480
+ Use a proxy pattern + HttpOnly cookies to keep tokens safe, centralize refresh logic, and provide a clean developer experience in your Next.js app. The snippets above can be copied directly and adjusted to your folder naming. Expand with caching & UI state management as needed.
481
+
482
+ If you need a tailored example repository scaffold, ask and we can generate it.
483
+
app/api/routes.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, HTTPException, status, Depends, Request
2
  from fastapi.responses import JSONResponse
3
  from typing import List, Dict, Any
4
  from datetime import datetime
@@ -36,9 +36,9 @@ router = APIRouter(prefix="/api/v1", tags=["hadith-analysis"])
36
  )
37
  @authenticated_user_limit()
38
  async def extract_narrators(
39
- request: HadithTextRequest,
40
  http_request: Request,
41
- current_user: User = Depends(get_current_active_user)
42
  ) -> NarratorExtractionResponse:
43
  """
44
  Extract narrators from hadith text.
@@ -59,12 +59,14 @@ async def extract_narrators(
59
  """
60
  start_time = time.time()
61
  db_service = DatabaseService()
62
-
63
  try:
64
  llm_service = get_llm_service()
65
  result = await llm_service.extract_narrators(request.hadith_text)
66
-
67
- processing_time = int((time.time() - start_time) * 1000) # Convert to milliseconds
 
 
68
 
69
  # Store extraction record in database
70
  extraction_record = NarratorExtractionRecord(
@@ -75,9 +77,9 @@ async def extract_narrators(
75
  success=result.success,
76
  error_message=result.message if not result.success else None,
77
  processing_time_ms=processing_time,
78
- ip_address=get_user_ip(http_request)
79
  )
80
-
81
  await db_service.store_extraction_record(extraction_record)
82
 
83
  if not result.success:
@@ -101,14 +103,14 @@ async def extract_narrators(
101
  success=False,
102
  error_message=str(e),
103
  processing_time_ms=processing_time,
104
- ip_address=get_user_ip(http_request)
105
  )
106
-
107
  try:
108
  await db_service.store_extraction_record(extraction_record)
109
  except:
110
  pass # Don't fail if database storage fails
111
-
112
  raise HTTPException(
113
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114
  detail=f"Internal server error during narrator extraction: {str(e)}",
@@ -124,7 +126,8 @@ async def extract_narrators(
124
  @authenticated_user_limit()
125
  async def analyze_narrator(
126
  request: NarratorAnalysisRequest,
127
- current_user: User = Depends(get_current_active_user)
 
128
  ) -> NarratorAnalysisResponse:
129
  """
130
  Analyze narrator reliability based on the model's internal knowledge.
@@ -145,11 +148,11 @@ async def analyze_narrator(
145
  """
146
  start_time = time.time()
147
  db_service = DatabaseService()
148
-
149
  try:
150
  llm_service = get_llm_service()
151
  result = await llm_service.analyze_narrator(request.narrator_name)
152
-
153
  processing_time = int((time.time() - start_time) * 1000)
154
 
155
  # Store analysis record in database
@@ -165,9 +168,9 @@ async def analyze_narrator(
165
  recommendation=result.recommendation if result.success else "",
166
  success=result.success,
167
  error_message=result.message if not result.success else None,
168
- processing_time_ms=processing_time
169
  )
170
-
171
  await db_service.store_analysis_record(analysis_record)
172
 
173
  if not result.success:
@@ -195,8 +198,9 @@ async def analyze_narrator(
195
  )
196
  @authenticated_user_limit()
197
  async def analyze_narrator_chain(
198
- narrator_names: List[str],
199
- current_user: User = Depends(get_current_active_user)
 
200
  ) -> NarratorChainAnalysisResponse:
201
  """
202
  Analyze a complete chain of narrators with enhanced data sources.
@@ -272,7 +276,8 @@ async def analyze_narrator_chain(
272
  @authenticated_user_limit()
273
  async def extract_and_analyze_hadith(
274
  request: HadithTextRequest,
275
- current_user: User = Depends(get_current_active_user)
 
276
  ) -> ExtractAndAnalyzeResponse:
277
  """
278
  Complete hadith analysis workflow: extraction + chain analysis.
@@ -377,7 +382,7 @@ async def health_check():
377
  "Complete hadith workflow analysis",
378
  "JWT Authentication",
379
  "Rate limiting",
380
- "Database storage"
381
  ],
382
  }
383
 
@@ -386,12 +391,13 @@ async def health_check():
386
  @router.get(
387
  "/user/extractions",
388
  summary="Get user's extraction history",
389
- description="Get the current user's narrator extraction history"
390
  )
391
  @authenticated_user_limit()
392
  async def get_user_extractions(
 
393
  current_user: User = Depends(get_current_active_user),
394
- limit: int = 50
395
  ):
396
  """Get user's extraction history."""
397
  db_service = DatabaseService()
@@ -402,12 +408,13 @@ async def get_user_extractions(
402
  @router.get(
403
  "/user/analyses",
404
  summary="Get user's analysis history",
405
- description="Get the current user's narrator analysis history"
406
  )
407
  @authenticated_user_limit()
408
  async def get_user_analyses(
 
409
  current_user: User = Depends(get_current_active_user),
410
- limit: int = 50
411
  ):
412
  """Get user's analysis history."""
413
  db_service = DatabaseService()
@@ -418,7 +425,7 @@ async def get_user_analyses(
418
  @router.get(
419
  "/analytics/stats",
420
  summary="Get extraction statistics",
421
- description="Get overall platform statistics (public)"
422
  )
423
  async def get_platform_stats():
424
  """Get platform-wide extraction statistics."""
@@ -430,7 +437,7 @@ async def get_platform_stats():
430
  @router.get(
431
  "/analytics/popular-narrators",
432
  summary="Get popular narrators",
433
- description="Get most frequently analyzed narrators (public)"
434
  )
435
  async def get_popular_narrators(limit: int = 10):
436
  """Get most analyzed narrators."""
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends, Request, Body
2
  from fastapi.responses import JSONResponse
3
  from typing import List, Dict, Any
4
  from datetime import datetime
 
36
  )
37
  @authenticated_user_limit()
38
  async def extract_narrators(
39
+ request: HadithTextRequest,
40
  http_request: Request,
41
+ current_user: User = Depends(get_current_active_user),
42
  ) -> NarratorExtractionResponse:
43
  """
44
  Extract narrators from hadith text.
 
59
  """
60
  start_time = time.time()
61
  db_service = DatabaseService()
62
+
63
  try:
64
  llm_service = get_llm_service()
65
  result = await llm_service.extract_narrators(request.hadith_text)
66
+
67
+ processing_time = int(
68
+ (time.time() - start_time) * 1000
69
+ ) # Convert to milliseconds
70
 
71
  # Store extraction record in database
72
  extraction_record = NarratorExtractionRecord(
 
77
  success=result.success,
78
  error_message=result.message if not result.success else None,
79
  processing_time_ms=processing_time,
80
+ ip_address=get_user_ip(http_request),
81
  )
82
+
83
  await db_service.store_extraction_record(extraction_record)
84
 
85
  if not result.success:
 
103
  success=False,
104
  error_message=str(e),
105
  processing_time_ms=processing_time,
106
+ ip_address=get_user_ip(http_request),
107
  )
108
+
109
  try:
110
  await db_service.store_extraction_record(extraction_record)
111
  except:
112
  pass # Don't fail if database storage fails
113
+
114
  raise HTTPException(
115
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
116
  detail=f"Internal server error during narrator extraction: {str(e)}",
 
126
  @authenticated_user_limit()
127
  async def analyze_narrator(
128
  request: NarratorAnalysisRequest,
129
+ http_request: Request,
130
+ current_user: User = Depends(get_current_active_user),
131
  ) -> NarratorAnalysisResponse:
132
  """
133
  Analyze narrator reliability based on the model's internal knowledge.
 
148
  """
149
  start_time = time.time()
150
  db_service = DatabaseService()
151
+
152
  try:
153
  llm_service = get_llm_service()
154
  result = await llm_service.analyze_narrator(request.narrator_name)
155
+
156
  processing_time = int((time.time() - start_time) * 1000)
157
 
158
  # Store analysis record in database
 
168
  recommendation=result.recommendation if result.success else "",
169
  success=result.success,
170
  error_message=result.message if not result.success else None,
171
+ processing_time_ms=processing_time,
172
  )
173
+
174
  await db_service.store_analysis_record(analysis_record)
175
 
176
  if not result.success:
 
198
  )
199
  @authenticated_user_limit()
200
  async def analyze_narrator_chain(
201
+ request: Request,
202
+ narrator_names: List[str] = Body(...),
203
+ current_user: User = Depends(get_current_active_user),
204
  ) -> NarratorChainAnalysisResponse:
205
  """
206
  Analyze a complete chain of narrators with enhanced data sources.
 
276
  @authenticated_user_limit()
277
  async def extract_and_analyze_hadith(
278
  request: HadithTextRequest,
279
+ http_request: Request,
280
+ current_user: User = Depends(get_current_active_user),
281
  ) -> ExtractAndAnalyzeResponse:
282
  """
283
  Complete hadith analysis workflow: extraction + chain analysis.
 
382
  "Complete hadith workflow analysis",
383
  "JWT Authentication",
384
  "Rate limiting",
385
+ "Database storage",
386
  ],
387
  }
388
 
 
391
  @router.get(
392
  "/user/extractions",
393
  summary="Get user's extraction history",
394
+ description="Get the current user's narrator extraction history",
395
  )
396
  @authenticated_user_limit()
397
  async def get_user_extractions(
398
+ request: Request,
399
  current_user: User = Depends(get_current_active_user),
400
+ limit: int = 50,
401
  ):
402
  """Get user's extraction history."""
403
  db_service = DatabaseService()
 
408
  @router.get(
409
  "/user/analyses",
410
  summary="Get user's analysis history",
411
+ description="Get the current user's narrator analysis history",
412
  )
413
  @authenticated_user_limit()
414
  async def get_user_analyses(
415
+ request: Request,
416
  current_user: User = Depends(get_current_active_user),
417
+ limit: int = 50,
418
  ):
419
  """Get user's analysis history."""
420
  db_service = DatabaseService()
 
425
  @router.get(
426
  "/analytics/stats",
427
  summary="Get extraction statistics",
428
+ description="Get overall platform statistics (public)",
429
  )
430
  async def get_platform_stats():
431
  """Get platform-wide extraction statistics."""
 
437
  @router.get(
438
  "/analytics/popular-narrators",
439
  summary="Get popular narrators",
440
+ description="Get most frequently analyzed narrators (public)",
441
  )
442
  async def get_popular_narrators(limit: int = 10):
443
  """Get most analyzed narrators."""
app/main.py CHANGED
@@ -116,5 +116,5 @@ async def shutdown_event():
116
  if __name__ == "__main__":
117
  # Run the application
118
  uvicorn.run(
119
- "app:app", host="0.0.0.0", port=8000, reload=settings.DEBUG, log_level="info"
120
  )
 
116
  if __name__ == "__main__":
117
  # Run the application
118
  uvicorn.run(
119
+ "app.main:app", host="0.0.0.0", port=8000, reload=settings.DEBUG, log_level="info"
120
  )