add auth
This commit is contained in:
@@ -10,23 +10,11 @@ class ApiConstants {
|
||||
// Base URLs
|
||||
// ============================================================================
|
||||
|
||||
/// Base URL for development environment
|
||||
static const String devBaseUrl = 'https://dev-api.worker.example.com';
|
||||
/// Base URL for all APIs (Frappe/ERPNext)
|
||||
static const String baseUrl = 'https://land.dbiz.com';
|
||||
|
||||
/// Base URL for staging environment
|
||||
static const String stagingBaseUrl = 'https://staging-api.worker.example.com';
|
||||
|
||||
/// Base URL for production environment
|
||||
static const String prodBaseUrl = 'https://api.worker.example.com';
|
||||
|
||||
/// Current base URL (should be configured based on build flavor)
|
||||
static const String baseUrl = devBaseUrl; // TODO: Configure with flavors
|
||||
|
||||
/// API version prefix
|
||||
static const String apiVersion = '/v1';
|
||||
|
||||
/// Full API base URL with version
|
||||
static String get apiBaseUrl => '$baseUrl$apiVersion';
|
||||
/// Full API base URL (no version prefix, using Frappe endpoints)
|
||||
static String get apiBaseUrl => baseUrl;
|
||||
|
||||
// ============================================================================
|
||||
// Timeout Configurations
|
||||
@@ -347,6 +335,35 @@ class ApiConstants {
|
||||
/// POST /promotions/{promotionId}/claim
|
||||
static const String claimPromotion = '/promotions';
|
||||
|
||||
// ============================================================================
|
||||
// Frappe/ERPNext API Endpoints
|
||||
// ============================================================================
|
||||
|
||||
/// Frappe API method prefix
|
||||
static const String frappeApiMethod = '/api/method';
|
||||
|
||||
/// Get Frappe session (public API, no auth required)
|
||||
/// POST /api/method/dbiz_common.dbiz_common.api.auth.get_session
|
||||
/// Returns: { "message": { "data": { "sid": "...", "csrf_token": "..." } }, "full_name": "..." }
|
||||
static const String frappeGetSession = '/dbiz_common.dbiz_common.api.auth.get_session';
|
||||
|
||||
/// Login with phone (requires session sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.auth.login
|
||||
/// Body: { "username": "phone", "googleid": null, "facebookid": null, "zaloid": null }
|
||||
/// Returns: { "message": { "data": { "sid": "...", "csrf_token": "..." } }, "full_name": "..." }
|
||||
static const String frappeLogin = '/building_material.building_material.api.auth.login';
|
||||
|
||||
/// Frappe client get_list (requires sid and csrf_token)
|
||||
/// POST /api/method/frappe.client.get_list
|
||||
static const String frappeGetList = '/frappe.client.get_list';
|
||||
|
||||
/// Register user (requires session sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.user.register
|
||||
static const String frappeRegister = '/building_material.building_material.api.user.register';
|
||||
|
||||
/// Frappe public API user ID
|
||||
static const String frappePublicUserId = 'public_api@dbiz.com';
|
||||
|
||||
// ============================================================================
|
||||
// Notification Endpoints
|
||||
// ============================================================================
|
||||
@@ -380,8 +397,8 @@ class ApiConstants {
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final url = ApiConstants.buildUrl('/products', {'page': '1', 'limit': '20'});
|
||||
/// // Returns: https://api.worker.example.com/v1/products?page=1&limit=20
|
||||
/// final url = ApiConstants.buildUrl('/api/method/frappe.client.get_list', {'doctype': 'Item'});
|
||||
/// // Returns: https://land.dbiz.com/api/method/frappe.client.get_list?doctype=Item
|
||||
/// ```
|
||||
static String buildUrl(String endpoint, [Map<String, String>? queryParams]) {
|
||||
final uri = Uri.parse('$apiBaseUrl$endpoint');
|
||||
@@ -395,8 +412,8 @@ class ApiConstants {
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final url = ApiConstants.buildUrlWithParams('/products/{id}', {'id': '123'});
|
||||
/// // Returns: https://api.worker.example.com/v1/products/123
|
||||
/// final url = ApiConstants.buildUrlWithParams('/api/resource/Item/{id}', {'id': '123'});
|
||||
/// // Returns: https://land.dbiz.com/api/resource/Item/123
|
||||
/// ```
|
||||
static String buildUrlWithParams(
|
||||
String endpoint,
|
||||
|
||||
95
lib/core/models/frappe_session_model.dart
Normal file
95
lib/core/models/frappe_session_model.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
/// Frappe Session Model
|
||||
///
|
||||
/// Data model for Frappe API session response.
|
||||
/// Used for public API authentication to access blog content.
|
||||
library;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'frappe_session_model.g.dart';
|
||||
|
||||
/// Frappe Session Data
|
||||
///
|
||||
/// Contains session credentials from Frappe API.
|
||||
@JsonSerializable()
|
||||
class FrappeSessionData {
|
||||
/// Session ID
|
||||
final String sid;
|
||||
|
||||
/// CSRF Token
|
||||
@JsonKey(name: 'csrf_token')
|
||||
final String csrfToken;
|
||||
|
||||
const FrappeSessionData({
|
||||
required this.sid,
|
||||
required this.csrfToken,
|
||||
});
|
||||
|
||||
factory FrappeSessionData.fromJson(Map<String, dynamic> json) =>
|
||||
_$FrappeSessionDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FrappeSessionDataToJson(this);
|
||||
}
|
||||
|
||||
/// Frappe Session Message Wrapper
|
||||
@JsonSerializable()
|
||||
class FrappeSessionMessage {
|
||||
/// Session data
|
||||
final FrappeSessionData data;
|
||||
|
||||
const FrappeSessionMessage({
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory FrappeSessionMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$FrappeSessionMessageFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FrappeSessionMessageToJson(this);
|
||||
}
|
||||
|
||||
/// Frappe Session Response
|
||||
///
|
||||
/// API response from get_session endpoint.
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": {
|
||||
/// "data": {
|
||||
/// "sid": "edb6059ecf147f268176cd4aff8ca034a75ebb8ff23464f9913c9537",
|
||||
/// "csrf_token": "d0077178c349f69bc1456401d9a3d90ef0f7b9df3e08cfd26794a53f"
|
||||
/// }
|
||||
/// },
|
||||
/// "home_page": "/app",
|
||||
/// "full_name": "PublicAPI"
|
||||
/// }
|
||||
/// ```
|
||||
@JsonSerializable()
|
||||
class FrappeSessionResponse {
|
||||
/// Message containing session data
|
||||
final FrappeSessionMessage message;
|
||||
|
||||
/// Home page path
|
||||
@JsonKey(name: 'home_page')
|
||||
final String homePage;
|
||||
|
||||
/// Full name of the API user
|
||||
@JsonKey(name: 'full_name')
|
||||
final String fullName;
|
||||
|
||||
const FrappeSessionResponse({
|
||||
required this.message,
|
||||
required this.homePage,
|
||||
required this.fullName,
|
||||
});
|
||||
|
||||
factory FrappeSessionResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$FrappeSessionResponseFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FrappeSessionResponseToJson(this);
|
||||
|
||||
/// Get session ID
|
||||
String get sid => message.data.sid;
|
||||
|
||||
/// Get CSRF token
|
||||
String get csrfToken => message.data.csrfToken;
|
||||
}
|
||||
62
lib/core/models/frappe_session_model.g.dart
Normal file
62
lib/core/models/frappe_session_model.g.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'frappe_session_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
FrappeSessionData _$FrappeSessionDataFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate('FrappeSessionData', json, ($checkedConvert) {
|
||||
final val = FrappeSessionData(
|
||||
sid: $checkedConvert('sid', (v) => v as String),
|
||||
csrfToken: $checkedConvert('csrf_token', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
}, fieldKeyMap: const {'csrfToken': 'csrf_token'});
|
||||
|
||||
Map<String, dynamic> _$FrappeSessionDataToJson(FrappeSessionData instance) =>
|
||||
<String, dynamic>{'sid': instance.sid, 'csrf_token': instance.csrfToken};
|
||||
|
||||
FrappeSessionMessage _$FrappeSessionMessageFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => $checkedCreate('FrappeSessionMessage', json, ($checkedConvert) {
|
||||
final val = FrappeSessionMessage(
|
||||
data: $checkedConvert(
|
||||
'data',
|
||||
(v) => FrappeSessionData.fromJson(v as Map<String, dynamic>),
|
||||
),
|
||||
);
|
||||
return val;
|
||||
});
|
||||
|
||||
Map<String, dynamic> _$FrappeSessionMessageToJson(
|
||||
FrappeSessionMessage instance,
|
||||
) => <String, dynamic>{'data': instance.data.toJson()};
|
||||
|
||||
FrappeSessionResponse _$FrappeSessionResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => $checkedCreate(
|
||||
'FrappeSessionResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = FrappeSessionResponse(
|
||||
message: $checkedConvert(
|
||||
'message',
|
||||
(v) => FrappeSessionMessage.fromJson(v as Map<String, dynamic>),
|
||||
),
|
||||
homePage: $checkedConvert('home_page', (v) => v as String),
|
||||
fullName: $checkedConvert('full_name', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {'homePage': 'home_page', 'fullName': 'full_name'},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FrappeSessionResponseToJson(
|
||||
FrappeSessionResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'message': instance.message.toJson(),
|
||||
'home_page': instance.homePage,
|
||||
'full_name': instance.fullName,
|
||||
};
|
||||
@@ -15,7 +15,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
|
||||
|
||||
part 'api_interceptor.g.dart';
|
||||
|
||||
@@ -35,15 +34,16 @@ class AuthStorageKeys {
|
||||
// Auth Interceptor
|
||||
// ============================================================================
|
||||
|
||||
/// Interceptor for adding ERPNext session tokens to requests
|
||||
/// Interceptor for adding Frappe/ERPNext session tokens to requests
|
||||
///
|
||||
/// Adds SID (Session ID) and CSRF token from Hive storage to request headers.
|
||||
/// Adds Cookie (with SID) and X-Frappe-CSRF-Token from FlutterSecureStorage.
|
||||
/// Uses the centralized FrappeAuthService for session management.
|
||||
class AuthInterceptor extends Interceptor {
|
||||
AuthInterceptor(this._prefs, this._dio, this._authLocalDataSource);
|
||||
AuthInterceptor(this._prefs, this._dio, this._secureStorage);
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
final Dio _dio;
|
||||
final AuthLocalDataSource _authLocalDataSource;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
@@ -52,13 +52,24 @@ class AuthInterceptor extends Interceptor {
|
||||
) async {
|
||||
// Check if this endpoint requires authentication
|
||||
if (_requiresAuth(options.path)) {
|
||||
// Get session data from secure storage (async)
|
||||
final sid = await _authLocalDataSource.getSid();
|
||||
final csrfToken = await _authLocalDataSource.getCsrfToken();
|
||||
// Get session data from secure storage
|
||||
final sid = await _secureStorage.read(key: 'frappe_sid');
|
||||
final csrfToken = await _secureStorage.read(key: 'frappe_csrf_token');
|
||||
final fullName = await _secureStorage.read(key: 'frappe_full_name');
|
||||
final userId = await _secureStorage.read(key: 'frappe_user_id');
|
||||
|
||||
if (sid != null && csrfToken != null) {
|
||||
// Add ERPNext session headers
|
||||
options.headers['Cookie'] = 'sid=$sid';
|
||||
// Build cookie header with all required fields
|
||||
final cookieHeader = [
|
||||
'sid=$sid',
|
||||
'full_name=${fullName ?? "User"}',
|
||||
'system_user=no',
|
||||
'user_id=${userId != null ? Uri.encodeComponent(userId) : ApiConstants.frappePublicUserId}',
|
||||
'user_image=',
|
||||
].join('; ');
|
||||
|
||||
// Add Frappe session headers
|
||||
options.headers['Cookie'] = cookieHeader;
|
||||
options.headers['X-Frappe-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
@@ -276,9 +287,20 @@ class LoggingInterceptor extends Interceptor {
|
||||
name: 'HTTP Response',
|
||||
);
|
||||
developer.log(
|
||||
'║ Data: ${_truncateData(response.data, 500)}',
|
||||
'║ Headers: ${response.headers.map}',
|
||||
name: 'HTTP Response',
|
||||
);
|
||||
|
||||
// Log full response data (not truncated for debugging)
|
||||
final responseData = response.data;
|
||||
if (responseData != null) {
|
||||
if (responseData is String) {
|
||||
developer.log('║ Response: $responseData', name: 'HTTP Response');
|
||||
} else {
|
||||
developer.log('║ Response: ${_truncateData(responseData, 2000)}', name: 'HTTP Response');
|
||||
}
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'╚══════════════════════════════════════════════════════════════',
|
||||
name: 'HTTP Response',
|
||||
@@ -534,14 +556,13 @@ Future<SharedPreferences> sharedPreferences(Ref ref) async {
|
||||
Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
||||
final prefs = await ref.watch(sharedPreferencesProvider.future);
|
||||
|
||||
// Create AuthLocalDataSource with FlutterSecureStorage
|
||||
// Use FlutterSecureStorage for Frappe session
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
final authLocalDataSource = AuthLocalDataSource(secureStorage);
|
||||
|
||||
return AuthInterceptor(prefs, dio, authLocalDataSource);
|
||||
return AuthInterceptor(prefs, dio, secureStorage);
|
||||
}
|
||||
|
||||
/// Provider for LoggingInterceptor
|
||||
|
||||
@@ -114,7 +114,7 @@ final class AuthInterceptorProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$authInterceptorHash() => r'3f964536e03e204d09cc9120dd9d961b6d6d4b71';
|
||||
String _$authInterceptorHash() => r'1221aab024b7c4d9fd393f7681f3ba094286a375';
|
||||
|
||||
/// Provider for AuthInterceptor
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/// - Retry logic
|
||||
library;
|
||||
|
||||
import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
|
||||
@@ -382,19 +383,21 @@ Future<Dio> dio(Ref ref) async {
|
||||
},
|
||||
)
|
||||
// Add interceptors in order
|
||||
// 1. Logging interceptor (first to log everything)
|
||||
// 1. Curl interceptor (first to log cURL commands)
|
||||
..interceptors.add(CurlLoggerDioInterceptor())
|
||||
// 2. Logging interceptor
|
||||
..interceptors.add(ref.watch(loggingInterceptorProvider))
|
||||
// 2. Auth interceptor (add tokens to requests)
|
||||
// 3. Auth interceptor (add tokens to requests)
|
||||
..interceptors.add(await ref.watch(authInterceptorProvider(dio).future))
|
||||
// 3. Cache interceptor
|
||||
// 4. Cache interceptor
|
||||
..interceptors.add(
|
||||
DioCacheInterceptor(
|
||||
options: await ref.watch(cacheOptionsProvider.future),
|
||||
),
|
||||
)
|
||||
// 4. Retry interceptor
|
||||
// 5. Retry interceptor
|
||||
..interceptors.add(RetryInterceptor(ref.watch(networkInfoProvider)))
|
||||
// 5. Error transformer (last to transform all errors)
|
||||
// 6. Error transformer (last to transform all errors)
|
||||
..interceptors.add(ref.watch(errorTransformerInterceptorProvider));
|
||||
|
||||
return dio;
|
||||
|
||||
@@ -131,7 +131,7 @@ final class DioProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
|
||||
String _$dioHash() => r'40bb4b1008c8259c9db4b19bcee674aa6732810c';
|
||||
|
||||
/// Provider for DioClient
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:worker/features/account/presentation/pages/addresses_page.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||
import 'package:worker/features/auth/domain/entities/business_unit.dart';
|
||||
@@ -39,20 +41,41 @@ import 'package:worker/features/showrooms/presentation/pages/design_request_crea
|
||||
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
||||
|
||||
/// App Router
|
||||
/// Router Provider
|
||||
///
|
||||
/// Handles navigation throughout the app using declarative routing.
|
||||
/// Features:
|
||||
/// - Named routes for type-safe navigation
|
||||
/// - Authentication guards (TODO: implement when auth is ready)
|
||||
/// - Deep linking support
|
||||
/// - Transition animations
|
||||
class AppRouter {
|
||||
/// Router configuration
|
||||
static final GoRouter router = GoRouter(
|
||||
/// Provides GoRouter instance with auth state management
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return GoRouter(
|
||||
// Initial route
|
||||
initialLocation: RouteNames.login,
|
||||
|
||||
// Redirect based on auth state
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = authState.value != null;
|
||||
final isOnLoginPage = state.matchedLocation == RouteNames.login;
|
||||
final isOnRegisterPage = state.matchedLocation == RouteNames.register;
|
||||
final isOnBusinessUnitPage =
|
||||
state.matchedLocation == RouteNames.businessUnitSelection;
|
||||
final isOnOtpPage = state.matchedLocation == RouteNames.otpVerification;
|
||||
final isOnAuthPage =
|
||||
isOnLoginPage || isOnRegisterPage || isOnBusinessUnitPage || isOnOtpPage;
|
||||
|
||||
// If not logged in and not on auth pages, redirect to login
|
||||
if (!isLoggedIn && !isOnAuthPage) {
|
||||
return RouteNames.login;
|
||||
}
|
||||
|
||||
// If logged in and on login page, redirect to home
|
||||
if (isLoggedIn && isOnLoginPage) {
|
||||
return RouteNames.home;
|
||||
}
|
||||
|
||||
// No redirect needed
|
||||
return null;
|
||||
},
|
||||
|
||||
// Route definitions
|
||||
routes: [
|
||||
// Authentication Routes
|
||||
@@ -384,26 +407,10 @@ class AppRouter {
|
||||
),
|
||||
),
|
||||
|
||||
// Redirect logic for authentication (TODO: implement when auth is ready)
|
||||
// redirect: (context, state) {
|
||||
// final isLoggedIn = false; // TODO: Get from auth provider
|
||||
// final isOnLoginPage = state.matchedLocation == RouteNames.login;
|
||||
//
|
||||
// if (!isLoggedIn && !isOnLoginPage) {
|
||||
// return RouteNames.login;
|
||||
// }
|
||||
//
|
||||
// if (isLoggedIn && isOnLoginPage) {
|
||||
// return RouteNames.home;
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// },
|
||||
|
||||
// Debug logging (disable in production)
|
||||
debugLogDiagnostics: true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// Route Names
|
||||
///
|
||||
|
||||
28
lib/core/services/frappe_auth_provider.dart
Normal file
28
lib/core/services/frappe_auth_provider.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
/// Frappe Auth Provider
|
||||
///
|
||||
/// Riverpod provider for FrappeAuthService.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/services/frappe_auth_service.dart';
|
||||
|
||||
part 'frappe_auth_provider.g.dart';
|
||||
|
||||
/// Frappe Auth Service Provider
|
||||
///
|
||||
/// Provides singleton instance of FrappeAuthService.
|
||||
@riverpod
|
||||
FrappeAuthService frappeAuthService(Ref ref) {
|
||||
// Create a separate Dio instance for Frappe auth
|
||||
// (not using the main dio client to avoid circular dependencies)
|
||||
final dio = Dio();
|
||||
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
|
||||
return FrappeAuthService(dio, secureStorage);
|
||||
}
|
||||
67
lib/core/services/frappe_auth_provider.g.dart
Normal file
67
lib/core/services/frappe_auth_provider.g.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'frappe_auth_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Frappe Auth Service Provider
|
||||
///
|
||||
/// Provides singleton instance of FrappeAuthService.
|
||||
|
||||
@ProviderFor(frappeAuthService)
|
||||
const frappeAuthServiceProvider = FrappeAuthServiceProvider._();
|
||||
|
||||
/// Frappe Auth Service Provider
|
||||
///
|
||||
/// Provides singleton instance of FrappeAuthService.
|
||||
|
||||
final class FrappeAuthServiceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
FrappeAuthService,
|
||||
FrappeAuthService,
|
||||
FrappeAuthService
|
||||
>
|
||||
with $Provider<FrappeAuthService> {
|
||||
/// Frappe Auth Service Provider
|
||||
///
|
||||
/// Provides singleton instance of FrappeAuthService.
|
||||
const FrappeAuthServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'frappeAuthServiceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$frappeAuthServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<FrappeAuthService> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FrappeAuthService create(Ref ref) {
|
||||
return frappeAuthService(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(FrappeAuthService value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<FrappeAuthService>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$frappeAuthServiceHash() => r'73112c920895302df011517e81c97eef2b5df5ac';
|
||||
245
lib/core/services/frappe_auth_service.dart
Normal file
245
lib/core/services/frappe_auth_service.dart
Normal file
@@ -0,0 +1,245 @@
|
||||
/// Frappe Authentication Service
|
||||
///
|
||||
/// Handles Frappe/ERPNext session management (sid and csrf_token).
|
||||
/// Provides methods to get session, login, and manage session storage.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/models/frappe_session_model.dart';
|
||||
|
||||
/// Frappe Auth Service
|
||||
///
|
||||
/// Manages Frappe session lifecycle:
|
||||
/// 1. Get initial session (public API)
|
||||
/// 2. Login with phone number
|
||||
/// 3. Store sid and csrf_token in secure storage
|
||||
/// 4. Provide session data for API requests
|
||||
class FrappeAuthService {
|
||||
FrappeAuthService(this._dio, this._secureStorage);
|
||||
|
||||
final Dio _dio;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
/// Storage keys for Frappe session
|
||||
static const String _keyFrappeSid = 'frappe_sid';
|
||||
static const String _keyFrappeCsrfToken = 'frappe_csrf_token';
|
||||
static const String _keyFrappeFullName = 'frappe_full_name';
|
||||
static const String _keyFrappeUserId = 'frappe_user_id';
|
||||
|
||||
/// Get Frappe session from API
|
||||
///
|
||||
/// This endpoint doesn't require authentication - it's public.
|
||||
/// Returns initial session for subsequent API calls.
|
||||
///
|
||||
/// API: POST /api/method/dbiz_common.dbiz_common.api.auth.get_session
|
||||
Future<FrappeSessionResponse> getSession() async {
|
||||
try {
|
||||
final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetSession}';
|
||||
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: '', // Empty data as per docs
|
||||
);
|
||||
|
||||
if (response.data == null) {
|
||||
throw Exception('Empty response from Frappe session API');
|
||||
}
|
||||
|
||||
final sessionResponse = FrappeSessionResponse.fromJson(response.data!);
|
||||
|
||||
// Store session in secure storage
|
||||
await _storeSession(
|
||||
sid: sessionResponse.sid,
|
||||
csrfToken: sessionResponse.csrfToken,
|
||||
fullName: sessionResponse.fullName,
|
||||
userId: ApiConstants.frappePublicUserId,
|
||||
);
|
||||
|
||||
return sessionResponse;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Failed to get Frappe session: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Unexpected error getting Frappe session: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Login with phone number
|
||||
///
|
||||
/// Requires existing session (sid and csrf_token).
|
||||
/// Returns new session with user's authentication.
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.auth.login
|
||||
/// Headers: Cookie (with sid), X-Frappe-Csrf-Token
|
||||
/// Body: { "username": "phone", "googleid": null, "facebookid": null, "zaloid": null }
|
||||
///
|
||||
/// Note: Password not used yet, but field reserved for future use
|
||||
Future<FrappeSessionResponse> login(String phone, {String? password}) async {
|
||||
try {
|
||||
// Ensure we have a session first
|
||||
final session = await getStoredSession();
|
||||
if (session == null) {
|
||||
await getSession();
|
||||
final newSession = await getStoredSession();
|
||||
if (newSession == null) {
|
||||
throw Exception('Failed to initialize session');
|
||||
}
|
||||
}
|
||||
|
||||
final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
|
||||
|
||||
// Build cookie header
|
||||
final storedSession = await getStoredSession();
|
||||
final cookieHeader = _buildCookieHeader(
|
||||
sid: storedSession!['sid']!,
|
||||
fullName: storedSession['fullName']!,
|
||||
userId: storedSession['userId']!,
|
||||
);
|
||||
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {
|
||||
'username': phone,
|
||||
'googleid': null,
|
||||
'facebookid': null,
|
||||
'zaloid': null,
|
||||
// Password field reserved for future use
|
||||
// 'password': password,
|
||||
},
|
||||
options: Options(
|
||||
headers: {
|
||||
'Cookie': cookieHeader,
|
||||
'X-Frappe-Csrf-Token': storedSession['csrfToken']!,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.data == null) {
|
||||
throw Exception('Empty response from login API');
|
||||
}
|
||||
|
||||
final loginResponse = FrappeSessionResponse.fromJson(response.data!);
|
||||
|
||||
// Store new session after login
|
||||
await _storeSession(
|
||||
sid: loginResponse.sid,
|
||||
csrfToken: loginResponse.csrfToken,
|
||||
fullName: loginResponse.fullName,
|
||||
userId: phone, // Use phone as userId after login
|
||||
);
|
||||
|
||||
return loginResponse;
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Login failed: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Unexpected error during login: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Store session in secure storage
|
||||
Future<void> _storeSession({
|
||||
required String sid,
|
||||
required String csrfToken,
|
||||
required String fullName,
|
||||
required String userId,
|
||||
}) async {
|
||||
await Future.wait([
|
||||
_secureStorage.write(key: _keyFrappeSid, value: sid),
|
||||
_secureStorage.write(key: _keyFrappeCsrfToken, value: csrfToken),
|
||||
_secureStorage.write(key: _keyFrappeFullName, value: fullName),
|
||||
_secureStorage.write(key: _keyFrappeUserId, value: userId),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Get stored session from secure storage
|
||||
Future<Map<String, String>?> getStoredSession() async {
|
||||
final results = await Future.wait([
|
||||
_secureStorage.read(key: _keyFrappeSid),
|
||||
_secureStorage.read(key: _keyFrappeCsrfToken),
|
||||
_secureStorage.read(key: _keyFrappeFullName),
|
||||
_secureStorage.read(key: _keyFrappeUserId),
|
||||
]);
|
||||
|
||||
final sid = results[0];
|
||||
final csrfToken = results[1];
|
||||
final fullName = results[2];
|
||||
final userId = results[3];
|
||||
|
||||
// Return null if session is incomplete
|
||||
if (sid == null || csrfToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
'sid': sid,
|
||||
'csrfToken': csrfToken,
|
||||
'fullName': fullName ?? 'User',
|
||||
'userId': userId ?? ApiConstants.frappePublicUserId,
|
||||
};
|
||||
}
|
||||
|
||||
/// Ensure valid session exists, fetch new one if needed
|
||||
Future<Map<String, String>> ensureSession() async {
|
||||
var session = await getStoredSession();
|
||||
|
||||
if (session == null) {
|
||||
// No session in storage, get a new one
|
||||
await getSession();
|
||||
session = await getStoredSession();
|
||||
if (session == null) {
|
||||
throw Exception('Failed to get session');
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// Check if session exists in storage
|
||||
Future<bool> hasSession() async {
|
||||
final sid = await _secureStorage.read(key: _keyFrappeSid);
|
||||
final csrfToken = await _secureStorage.read(key: _keyFrappeCsrfToken);
|
||||
return sid != null && csrfToken != null;
|
||||
}
|
||||
|
||||
/// Build cookie header string
|
||||
String _buildCookieHeader({
|
||||
required String sid,
|
||||
required String fullName,
|
||||
required String userId,
|
||||
}) {
|
||||
return [
|
||||
'sid=$sid',
|
||||
'full_name=$fullName',
|
||||
'system_user=no',
|
||||
'user_id=${Uri.encodeComponent(userId)}',
|
||||
'user_image=',
|
||||
].join('; ');
|
||||
}
|
||||
|
||||
/// Get headers for Frappe API requests
|
||||
Future<Map<String, String>> getHeaders() async {
|
||||
final session = await ensureSession();
|
||||
|
||||
return {
|
||||
'Cookie': _buildCookieHeader(
|
||||
sid: session['sid']!,
|
||||
fullName: session['fullName']!,
|
||||
userId: session['userId']!,
|
||||
),
|
||||
'X-Frappe-Csrf-Token': session['csrfToken']!,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear stored session
|
||||
Future<void> clearSession() async {
|
||||
await Future.wait([
|
||||
_secureStorage.delete(key: _keyFrappeSid),
|
||||
_secureStorage.delete(key: _keyFrappeCsrfToken),
|
||||
_secureStorage.delete(key: _keyFrappeFullName),
|
||||
_secureStorage.delete(key: _keyFrappeUserId),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user