**# 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 getSession(); // Get public session Future login(String phone, {String? password}); // Login Future?> getStoredSession(); // Read from storage Future> ensureSession(); // Ensure session exists Future> getHeaders(); // Get headers for API calls Future clearSession(); // Clear on logout } ``` **AuthRemoteDataSource**: ```dart class AuthRemoteDataSource { Future getSession(); // Wrapper for Frappe getSession Future login({phone, csrfToken, sid, password}); // Login API Future> getCities({csrfToken, sid}); // Get cities for registration Future> getCustomerGroups({csrfToken, sid}); // Get customer groups Future> register({...}); // Register new user } ``` **Auth Provider**: ```dart @riverpod class Auth extends _$Auth { @override Future build(); // Initialize session on app startup Future login({phoneNumber, password}); // Login flow Future 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 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`.**