Files
worker/docs/md/AUTH_FLOW.md
Phuoc Nguyen 65f6f825a6 update md
2025-11-28 15:16:40 +07:00

448 lines
13 KiB
Markdown

**# Authentication Flow - Frappe/ERPNext Integration
## Overview
The authentication system integrates with Frappe/ERPNext API using a session-based approach with SID (Session ID) and CSRF tokens stored in FlutterSecureStorage.
## Complete Flow
### 1. App Startup (Check Saved Session)
**When**: User opens the app
**Process**:
1. `Auth` provider's `build()` method is called
2. Checks if user session exists in FlutterSecureStorage
3. If logged-in session exists (userId != public_api@dbiz.com), returns User entity
4. Otherwise returns `null` (user not logged in)
5. **Note**: Public session is NOT fetched on startup to avoid provider disposal issues
**Important**: The public session will be fetched lazily when needed:
- Before login (on login page load)
- Before registration (when loading cities/customer groups)
- Before any API call that requires session (via `ensureSession()`)
**API Endpoint**: `POST /api/method/dbiz_common.dbiz_common.api.auth.get_session`
**Request**:
```bash
curl -X POST 'https://land.dbiz.com/api/method/dbiz_common.dbiz_common.api.auth.get_session' \
-H 'Content-Type: application/json' \
-d ''
```
**Response**:
```json
{
"session_expired": 1,
"message": {
"data": {
"sid": "8c39b583...",
"csrf_token": "f8a7754a9ce5..."
}
},
"home_page": "/app",
"full_name": "Guest"
}
```
**Storage** (FlutterSecureStorage):
- `frappe_sid`: "8c39b583..."
- `frappe_csrf_token`: "f8a7754a9ce5..."
- `frappe_full_name`: "Guest"
- `frappe_user_id`: "public_api@dbiz.com"
---
### 2. Initialize Public Session (When Needed)
**When**: Before login or registration, or before any API call
**Process**:
1. Call `ref.read(initializeFrappeSessionProvider.future)` on the page
2. Checks if session exists in FlutterSecureStorage
3. If no session, calls `FrappeAuthService.getSession()`
4. Stores public session (sid, csrf_token) in FlutterSecureStorage
**Usage Example**:
```dart
// In login page or registration page initState/useEffect
@override
void initState() {
super.initState();
// Initialize session when page loads
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(initializeFrappeSessionProvider.future);
});
}
```
Or use `FutureBuilder`:
```dart
FutureBuilder(
future: ref.read(initializeFrappeSessionProvider.future),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return LoadingIndicator();
}
return LoginForm(); // or RegistrationForm
},
)
```
### 3. Loading Cities & Customer Groups (Using Public Session)
**When**: User navigates to registration screen
**Process**:
1. Session initialized (if not already) via `initializeFrappeSessionProvider`
2. `AuthRemoteDataSource.getCities()` is called
3. Gets stored session from FlutterSecureStorage
4. Calls API with session headers
5. Returns list of cities for address selection
**API Endpoint**: `POST /api/method/frappe.client.get_list`
**Request**:
```bash
curl -X POST 'https://land.dbiz.com/api/method/frappe.client.get_list' \
-H 'Cookie: sid=8c39b583...; full_name=Guest; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
-H 'X-Frappe-CSRF-Token: f8a7754a9ce5...' \
-H 'Content-Type: application/json' \
-d '{
"doctype": "City",
"fields": ["city_name", "name", "code"],
"limit_page_length": 0
}'
```
**Response**:
```json
{
"message": [
{"city_name": "Hồ Chí Minh", "name": "HCM", "code": "HCM"},
{"city_name": "Hà Nội", "name": "HN", "code": "HN"}
]
}
```
**Similarly for Customer Groups**:
```json
{
"doctype": "Customer Group",
"fields": ["customer_group_name", "name", "value"],
"filters": {
"is_group": 0,
"is_active": 1,
"customer": 1
}
}
```
---
### 4. User Login (Get Authenticated Session)
**When**: User enters phone number and password, clicks login
**Process**:
1. `Auth.login()` is called with phone number
2. Gets current session from FlutterSecureStorage
3. Calls `AuthRemoteDataSource.login()` with phone + current session
4. API returns new authenticated session
5. `FrappeAuthService.login()` stores new session in FlutterSecureStorage
6. Dio interceptor automatically uses new session for all subsequent requests
7. Returns `User` entity with user data
**API Endpoint**: `POST /api/method/building_material.building_material.api.auth.login`
**Request**:
```bash
curl -X POST 'https://land.dbiz.com/api/method/building_material.building_material.api.auth.login' \
-H 'Cookie: sid=8c39b583...' \
-H 'X-Frappe-CSRF-Token: f8a7754a9ce5...' \
-H 'Content-Type: application/json' \
-d '{
"username": "0123456789",
"googleid": null,
"facebookid": null,
"zaloid": null
}'
```
**Response**:
```json
{
"session_expired": 1,
"message": {
"data": {
"sid": "new_authenticated_sid_123...",
"csrf_token": "new_csrf_token_456..."
}
},
"home_page": "/app",
"full_name": "Nguyễn Văn A"
}
```
**Storage Update** (FlutterSecureStorage):
- `frappe_sid`: "new_authenticated_sid_123..."
- `frappe_csrf_token`: "new_csrf_token_456..."
- `frappe_full_name`: "Nguyễn Văn A"
- `frappe_user_id`: "0123456789"
---
### 5. Authenticated API Requests
**When**: User makes any API request after login
**Process**:
1. `AuthInterceptor.onRequest()` is called
2. Reads session from FlutterSecureStorage
3. Builds cookie header with all required fields
4. Adds headers to request
**Cookie Header Format**:
```
Cookie: sid=new_authenticated_sid_123...; full_name=Nguyễn Văn A; system_user=no; user_id=0123456789; user_image=
X-Frappe-CSRF-Token: new_csrf_token_456...
```
**Example**: Getting products
```bash
curl -X POST 'https://land.dbiz.com/api/method/frappe.client.get_list' \
-H 'Cookie: sid=new_authenticated_sid_123...; full_name=Nguyễn%20Văn%20A; system_user=no; user_id=0123456789; user_image=' \
-H 'X-Frappe-CSRF-Token: new_csrf_token_456...' \
-H 'Content-Type: application/json' \
-d '{
"doctype": "Item",
"fields": ["item_name", "item_code", "standard_rate"],
"limit_page_length": 20
}'
```
---
### 6. User Logout
**When**: User clicks logout button
**Process**:
1. `Auth.logout()` is called
2. Clears session from both:
- `AuthLocalDataSource` (legacy Hive)
- `FrappeAuthService` (FlutterSecureStorage)
3. Gets new public session for next login/registration
4. Returns `null` (user logged out)
**Storage Cleared**:
- `frappe_sid`
- `frappe_csrf_token`
- `frappe_full_name`
- `frappe_user_id`
**New Public Session**: Immediately calls `getSession()` again to get fresh public session
---
## File Structure
### Core Services
- `lib/core/services/frappe_auth_service.dart` - Centralized session management
- `lib/core/models/frappe_session_model.dart` - Session response model
- `lib/core/network/api_interceptor.dart` - Dio interceptor for adding session headers
### Auth Feature
- `lib/features/auth/data/datasources/auth_remote_datasource.dart` - API calls (login, getCities, getCustomerGroups, register)
- `lib/features/auth/data/datasources/auth_local_datasource.dart` - Legacy Hive storage
- `lib/features/auth/presentation/providers/auth_provider.dart` - State management
### Key Components
**FrappeAuthService**:
```dart
class FrappeAuthService {
Future<FrappeSessionResponse> getSession(); // Get public session
Future<FrappeSessionResponse> login(String phone, {String? password}); // Login
Future<Map<String, String>?> getStoredSession(); // Read from storage
Future<Map<String, String>> ensureSession(); // Ensure session exists
Future<Map<String, String>> getHeaders(); // Get headers for API calls
Future<void> clearSession(); // Clear on logout
}
```
**AuthRemoteDataSource**:
```dart
class AuthRemoteDataSource {
Future<GetSessionResponse> getSession(); // Wrapper for Frappe getSession
Future<GetSessionResponse> login({phone, csrfToken, sid, password}); // Login API
Future<List<City>> getCities({csrfToken, sid}); // Get cities for registration
Future<List<CustomerGroup>> getCustomerGroups({csrfToken, sid}); // Get customer groups
Future<Map<String, dynamic>> register({...}); // Register new user
}
```
**Auth Provider**:
```dart
@riverpod
class Auth extends _$Auth {
@override
Future<User?> build(); // Initialize session on app startup
Future<void> login({phoneNumber, password}); // Login flow
Future<void> logout(); // Logout and get new public session
}
```
**AuthInterceptor**:
```dart
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Read from FlutterSecureStorage
// Build cookie header
// Add to request headers
}
}
```
---
## Session Storage
All session data is stored in **FlutterSecureStorage** (encrypted):
| Key | Description | Example |
|-----|-------------|---------|
| `frappe_sid` | Session ID | "8c39b583..." |
| `frappe_csrf_token` | CSRF Token | "f8a7754a9ce5..." |
| `frappe_full_name` | User's full name | "Nguyễn Văn A" |
| `frappe_user_id` | User ID (phone or email) | "0123456789" or "public_api@dbiz.com" |
---
## Public vs Authenticated Session
### Public Session
- **User ID**: `public_api@dbiz.com`
- **Full Name**: "Guest"
- **Used for**: Registration, loading cities/customer groups
- **Obtained**: On app startup, after logout
### Authenticated Session
- **User ID**: User's phone number (e.g., "0123456789")
- **Full Name**: User's actual name (e.g., "Nguyễn Văn A")
- **Used for**: All user-specific operations (orders, cart, profile)
- **Obtained**: After successful login
---
## Error Handling
All API calls use proper exception handling:
- **401 Unauthorized**: `UnauthorizedException` - Session expired or invalid
- **404 Not Found**: `NotFoundException` - Endpoint not found
- **Network errors**: `NetworkException` - Connection failed
- **Validation errors**: `ValidationException` - Invalid data
---
## Future Enhancements
1. **Password Support**: Currently reserved but not sent. When backend supports password:
```dart
Future<GetSessionResponse> login({
required String phone,
required String csrfToken,
required String sid,
String? password, // Remove nullable, make required
}) async {
// Add 'password': password to request body
}
```
2. **Token Refresh**: Implement automatic token refresh on 401 errors
3. **Session Expiry**: Add session expiry tracking and automatic re-authentication
4. **Biometric Login**: Store phone number and use biometric for quick re-login
---
## Testing the Flow
### 1. Test Public Session
```dart
final frappeService = ref.read(frappeAuthServiceProvider).value!;
final session = await frappeService.getSession();
print('SID: ${session.sid}');
print('CSRF: ${session.csrfToken}');
```
### 2. Test Login
```dart
final auth = ref.read(authProvider.notifier);
await auth.login(
phoneNumber: '0123456789',
password: 'not_used_yet',
);
```
### 3. Test Authenticated Request
```dart
final remoteDataSource = ref.read(authRemoteDataSourceProvider).value!;
final cities = await remoteDataSource.getCities(
csrfToken: 'from_storage',
sid: 'from_storage',
);
```
### 4. Test Logout
```dart
await ref.read(authProvider.notifier).logout();
```
---
## Debugging
Enable cURL logging to see all requests:
**In `dio_client.dart`**:
```dart
dio.interceptors.add(CurlLoggerDioInterceptor());
```
**Console Output**:
```
╔══════════════════════════════════════════════════════════════
║ POST https://land.dbiz.com/api/method/building_material.building_material.api.auth.login
║ Headers: {Cookie: [HIDDEN], X-Frappe-CSRF-Token: [HIDDEN], ...}
║ Body: {username: 0123456789, googleid: null, ...}
╚══════════════════════════════════════════════════════════════
╔══════════════════════════════════════════════════════════════
║ Response: {session_expired: 1, message: {...}, full_name: Nguyễn Văn A}
╚══════════════════════════════════════════════════════════════
```
---
## Summary
The authentication flow is now fully integrated with Frappe/ERPNext:
1. ✅ App startup checks for saved user session
2. ✅ Public session fetched lazily when needed (via `initializeFrappeSessionProvider`)
3. ✅ Public session used for cities/customer groups
4. ✅ Login updates session to authenticated
5. ✅ All API requests use session from FlutterSecureStorage
6. ✅ Dio interceptor automatically adds headers
7. ✅ Logout clears session and gets new public session
8. ✅ cURL logging for debugging
9. ✅ No provider disposal errors
All session management is centralized in `FrappeAuthService` with automatic integration via `AuthInterceptor`.**