add auth
This commit is contained in:
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