update md
This commit is contained in:
447
docs/md/AUTH_FLOW.md
Normal file
447
docs/md/AUTH_FLOW.md
Normal file
@@ -0,0 +1,447 @@
|
||||
**# 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`.**
|
||||
Reference in New Issue
Block a user