fill
This commit is contained in:
13
lib/features/auth/data/data.dart
Normal file
13
lib/features/auth/data/data.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
/// Barrel file for auth data layer exports
|
||||
///
|
||||
/// Provides clean imports for data layer components
|
||||
|
||||
// Data sources
|
||||
export 'datasources/auth_remote_datasource.dart';
|
||||
|
||||
// Models
|
||||
export 'models/login_request_model.dart';
|
||||
export 'models/user_model.dart';
|
||||
|
||||
// Repositories
|
||||
export 'repositories/auth_repository_impl.dart';
|
||||
147
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
147
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/network/api_response.dart';
|
||||
import '../models/login_request_model.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// Abstract interface for authentication remote data source
|
||||
///
|
||||
/// Defines the contract for authentication-related API operations
|
||||
abstract class AuthRemoteDataSource {
|
||||
/// Login with username and password
|
||||
///
|
||||
/// Throws [ServerException] if the login fails
|
||||
/// Returns [UserModel] on successful login
|
||||
Future<UserModel> login(LoginRequestModel request);
|
||||
|
||||
/// Logout current user
|
||||
///
|
||||
/// Throws [ServerException] if logout fails
|
||||
Future<void> logout();
|
||||
|
||||
/// Refresh access token using refresh token
|
||||
///
|
||||
/// Throws [ServerException] if refresh fails
|
||||
/// Returns new [UserModel] with updated tokens
|
||||
Future<UserModel> refreshToken(String refreshToken);
|
||||
}
|
||||
|
||||
/// Implementation of AuthRemoteDataSource using ApiClient
|
||||
///
|
||||
/// Handles all authentication-related API calls
|
||||
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
AuthRemoteDataSourceImpl(this.apiClient);
|
||||
|
||||
@override
|
||||
Future<UserModel> login(LoginRequestModel request) async {
|
||||
try {
|
||||
// Make POST request to login endpoint
|
||||
final response = await apiClient.post(
|
||||
ApiEndpoints.login,
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
// Parse API response with ApiResponse wrapper
|
||||
final apiResponse = ApiResponse.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(json) => UserModel.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
username: request.username, // Pass username since API doesn't return it
|
||||
),
|
||||
);
|
||||
|
||||
// Check if login was successful
|
||||
if (apiResponse.isSuccess && apiResponse.value != null) {
|
||||
return apiResponse.value!;
|
||||
} else {
|
||||
// Extract error message from API response
|
||||
final errorMessage = apiResponse.errors.isNotEmpty
|
||||
? apiResponse.errors.first
|
||||
: 'Login failed';
|
||||
|
||||
throw ServerException(
|
||||
errorMessage,
|
||||
code: apiResponse.errorCodes.isNotEmpty
|
||||
? apiResponse.errorCodes.first
|
||||
: null,
|
||||
);
|
||||
}
|
||||
} on ServerException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to login: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
// Make POST request to logout endpoint
|
||||
final response = await apiClient.post(ApiEndpoints.logout);
|
||||
|
||||
// Parse API response
|
||||
final apiResponse = ApiResponse.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
null,
|
||||
);
|
||||
|
||||
// Check if logout was successful
|
||||
if (!apiResponse.isSuccess) {
|
||||
final errorMessage = apiResponse.errors.isNotEmpty
|
||||
? apiResponse.errors.first
|
||||
: 'Logout failed';
|
||||
|
||||
throw ServerException(
|
||||
errorMessage,
|
||||
code: apiResponse.errorCodes.isNotEmpty
|
||||
? apiResponse.errorCodes.first
|
||||
: null,
|
||||
);
|
||||
}
|
||||
} on ServerException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to logout: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserModel> refreshToken(String refreshToken) async {
|
||||
try {
|
||||
// Make POST request to refresh token endpoint
|
||||
final response = await apiClient.post(
|
||||
ApiEndpoints.refreshToken,
|
||||
data: {'refreshToken': refreshToken},
|
||||
);
|
||||
|
||||
// Parse API response
|
||||
final apiResponse = ApiResponse.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(json) => UserModel.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
// Check if refresh was successful
|
||||
if (apiResponse.isSuccess && apiResponse.value != null) {
|
||||
return apiResponse.value!;
|
||||
} else {
|
||||
final errorMessage = apiResponse.errors.isNotEmpty
|
||||
? apiResponse.errors.first
|
||||
: 'Token refresh failed';
|
||||
|
||||
throw ServerException(
|
||||
errorMessage,
|
||||
code: apiResponse.errorCodes.isNotEmpty
|
||||
? apiResponse.errorCodes.first
|
||||
: null,
|
||||
);
|
||||
}
|
||||
} on ServerException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to refresh token: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/features/auth/data/models/login_request_model.dart
Normal file
42
lib/features/auth/data/models/login_request_model.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Login request model for authentication
|
||||
///
|
||||
/// Contains the credentials required for user login
|
||||
class LoginRequestModel extends Equatable {
|
||||
/// Username for authentication
|
||||
final String username;
|
||||
|
||||
/// Password for authentication
|
||||
final String password;
|
||||
|
||||
const LoginRequestModel({
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
/// Convert to JSON for API request
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'EmailPhone': username,
|
||||
'Password': password,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
LoginRequestModel copyWith({
|
||||
String? username,
|
||||
String? password,
|
||||
}) {
|
||||
return LoginRequestModel(
|
||||
username: username ?? this.username,
|
||||
password: password ?? this.password,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [username, password];
|
||||
|
||||
@override
|
||||
String toString() => 'LoginRequestModel(username: $username)';
|
||||
}
|
||||
81
lib/features/auth/data/models/user_model.dart
Normal file
81
lib/features/auth/data/models/user_model.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
/// User model that extends UserEntity for data layer
|
||||
///
|
||||
/// Handles JSON serialization/deserialization for API responses
|
||||
class UserModel extends UserEntity {
|
||||
const UserModel({
|
||||
required super.userId,
|
||||
required super.username,
|
||||
required super.accessToken,
|
||||
super.refreshToken,
|
||||
});
|
||||
|
||||
/// Create UserModel from JSON response
|
||||
///
|
||||
/// Expected JSON format from API:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "AccessToken": "string"
|
||||
/// }
|
||||
/// ```
|
||||
factory UserModel.fromJson(Map<String, dynamic> json, {String? username}) {
|
||||
return UserModel(
|
||||
userId: username ?? 'user', // Use username as userId or default
|
||||
username: username ?? 'user',
|
||||
accessToken: json['AccessToken'] as String,
|
||||
refreshToken: null, // API doesn't provide refresh token
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert UserModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'username': username,
|
||||
'accessToken': accessToken,
|
||||
'refreshToken': refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create UserModel from UserEntity
|
||||
factory UserModel.fromEntity(UserEntity entity) {
|
||||
return UserModel(
|
||||
userId: entity.userId,
|
||||
username: entity.username,
|
||||
accessToken: entity.accessToken,
|
||||
refreshToken: entity.refreshToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to UserEntity
|
||||
UserEntity toEntity() {
|
||||
return UserEntity(
|
||||
userId: userId,
|
||||
username: username,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
@override
|
||||
UserModel copyWith({
|
||||
String? userId,
|
||||
String? username,
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
}) {
|
||||
return UserModel(
|
||||
userId: userId ?? this.userId,
|
||||
username: username ?? this.username,
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserModel(userId: $userId, username: $username, hasRefreshToken: ${refreshToken != null})';
|
||||
}
|
||||
}
|
||||
134
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
134
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/storage/secure_storage.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../datasources/auth_remote_datasource.dart';
|
||||
import '../models/login_request_model.dart';
|
||||
|
||||
/// Implementation of AuthRepository
|
||||
///
|
||||
/// Coordinates between remote data source and local storage
|
||||
/// Handles error conversion from exceptions to failures
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final AuthRemoteDataSource remoteDataSource;
|
||||
final SecureStorage secureStorage;
|
||||
|
||||
AuthRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
required this.secureStorage,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, UserEntity>> login(LoginRequestModel request) async {
|
||||
try {
|
||||
// Call remote data source to login
|
||||
final userModel = await remoteDataSource.login(request);
|
||||
|
||||
// Save tokens to secure storage
|
||||
await secureStorage.saveAccessToken(userModel.accessToken);
|
||||
await secureStorage.saveUserId(userModel.userId);
|
||||
await secureStorage.saveUsername(userModel.username);
|
||||
|
||||
if (userModel.refreshToken != null) {
|
||||
await secureStorage.saveRefreshToken(userModel.refreshToken!);
|
||||
}
|
||||
|
||||
// Return user entity
|
||||
return Right(userModel.toEntity());
|
||||
} on ServerException catch (e) {
|
||||
return Left(AuthenticationFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure('Login failed: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> logout() async {
|
||||
try {
|
||||
// Call remote data source to logout (optional - can fail silently)
|
||||
try {
|
||||
await remoteDataSource.logout();
|
||||
} catch (e) {
|
||||
// Ignore remote logout errors, still clear local data
|
||||
}
|
||||
|
||||
// Clear all local authentication data
|
||||
await secureStorage.clearAll();
|
||||
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure('Logout failed: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, UserEntity>> refreshToken(String refreshToken) async {
|
||||
try {
|
||||
// Call remote data source to refresh token
|
||||
final userModel = await remoteDataSource.refreshToken(refreshToken);
|
||||
|
||||
// Update tokens in secure storage
|
||||
await secureStorage.saveAccessToken(userModel.accessToken);
|
||||
if (userModel.refreshToken != null) {
|
||||
await secureStorage.saveRefreshToken(userModel.refreshToken!);
|
||||
}
|
||||
|
||||
return Right(userModel.toEntity());
|
||||
} on ServerException catch (e) {
|
||||
return Left(AuthenticationFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure('Token refresh failed: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isAuthenticated() async {
|
||||
try {
|
||||
return await secureStorage.isAuthenticated();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, UserEntity>> getCurrentUser() async {
|
||||
try {
|
||||
final userId = await secureStorage.getUserId();
|
||||
final username = await secureStorage.getUsername();
|
||||
final accessToken = await secureStorage.getAccessToken();
|
||||
final refreshToken = await secureStorage.getRefreshToken();
|
||||
|
||||
if (userId == null || username == null || accessToken == null) {
|
||||
return const Left(AuthenticationFailure('No user data found'));
|
||||
}
|
||||
|
||||
final user = UserEntity(
|
||||
userId: userId,
|
||||
username: username,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
);
|
||||
|
||||
return Right(user);
|
||||
} catch (e) {
|
||||
return Left(CacheFailure('Failed to get user data: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> clearAuthData() async {
|
||||
try {
|
||||
await secureStorage.clearAll();
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
return Left(CacheFailure('Failed to clear auth data: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user