update api

This commit is contained in:
Phuoc Nguyen
2025-10-10 17:15:40 +07:00
parent b94c158004
commit 04f7042b8d
24 changed files with 3322 additions and 8 deletions

View File

@@ -0,0 +1,159 @@
import 'package:dio/dio.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/dio_client.dart';
import '../models/auth_response_model.dart';
import '../models/login_dto.dart';
import '../models/register_dto.dart';
import '../models/user_model.dart';
/// Remote data source for authentication operations
abstract class AuthRemoteDataSource {
/// Login user with email and password
Future<AuthResponseModel> login(LoginDto loginDto);
/// Register new user
Future<AuthResponseModel> register(RegisterDto registerDto);
/// Get current user profile
Future<UserModel> getProfile();
/// Refresh access token
Future<AuthResponseModel> refreshToken();
}
/// Implementation of AuthRemoteDataSource
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final DioClient dioClient;
AuthRemoteDataSourceImpl({required this.dioClient});
@override
Future<AuthResponseModel> login(LoginDto loginDto) async {
try {
final response = await dioClient.post(
ApiConstants.login,
data: loginDto.toJson(),
);
if (response.statusCode == ApiConstants.statusOk) {
return AuthResponseModel.fromJson(response.data);
} else {
throw ServerException('Login failed with status: ${response.statusCode}');
}
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Unexpected error during login: $e');
}
}
@override
Future<AuthResponseModel> register(RegisterDto registerDto) async {
try {
final response = await dioClient.post(
ApiConstants.register,
data: registerDto.toJson(),
);
if (response.statusCode == ApiConstants.statusCreated) {
return AuthResponseModel.fromJson(response.data);
} else {
throw ServerException('Registration failed with status: ${response.statusCode}');
}
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Unexpected error during registration: $e');
}
}
@override
Future<UserModel> getProfile() async {
try {
final response = await dioClient.get(ApiConstants.profile);
if (response.statusCode == ApiConstants.statusOk) {
return UserModel.fromJson(response.data);
} else {
throw ServerException('Get profile failed with status: ${response.statusCode}');
}
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Unexpected error getting profile: $e');
}
}
@override
Future<AuthResponseModel> refreshToken() async {
try {
final response = await dioClient.post(ApiConstants.refreshToken);
if (response.statusCode == ApiConstants.statusOk) {
return AuthResponseModel.fromJson(response.data);
} else {
throw ServerException('Token refresh failed with status: ${response.statusCode}');
}
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Unexpected error refreshing token: $e');
}
}
/// Handle Dio errors and convert to custom exceptions
Exception _handleDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return NetworkException('Connection timeout. Please check your internet connection.');
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
final message = error.response?.data?['message'] ?? error.message;
switch (statusCode) {
case ApiConstants.statusUnauthorized:
return InvalidCredentialsException(message ?? 'Invalid email or password');
case ApiConstants.statusForbidden:
return UnauthorizedException(message ?? 'Access forbidden');
case ApiConstants.statusNotFound:
return NotFoundException(message ?? 'Resource not found');
case ApiConstants.statusUnprocessableEntity:
return ValidationException(message ?? 'Validation failed');
case 409: // Conflict
return ConflictException(message ?? 'Email already exists');
case ApiConstants.statusTooManyRequests:
return ServerException('Too many requests. Please try again later.');
case ApiConstants.statusInternalServerError:
case ApiConstants.statusBadGateway:
case ApiConstants.statusServiceUnavailable:
case ApiConstants.statusGatewayTimeout:
return ServerException(message ?? 'Server error. Please try again later.');
default:
return ServerException(message ?? 'Unknown error occurred');
}
case DioExceptionType.connectionError:
return NetworkException('No internet connection. Please check your network.');
case DioExceptionType.badCertificate:
return NetworkException('SSL certificate error');
case DioExceptionType.cancel:
return NetworkException('Request was cancelled');
default:
return ServerException(error.message ?? 'Unknown error occurred');
}
}
}

View File

@@ -0,0 +1,42 @@
import '../../domain/entities/auth_response.dart';
import 'user_model.dart';
/// AuthResponse model for data layer (extends AuthResponse entity)
class AuthResponseModel extends AuthResponse {
const AuthResponseModel({
required super.accessToken,
required super.user,
});
/// Create AuthResponseModel from JSON
factory AuthResponseModel.fromJson(Map<String, dynamic> json) {
return AuthResponseModel(
accessToken: json['access_token'] as String,
user: UserModel.fromJson(json['user'] as Map<String, dynamic>),
);
}
/// Convert AuthResponseModel to JSON
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'user': (user as UserModel).toJson(),
};
}
/// Create AuthResponseModel from AuthResponse entity
factory AuthResponseModel.fromEntity(AuthResponse authResponse) {
return AuthResponseModel(
accessToken: authResponse.accessToken,
user: authResponse.user,
);
}
/// Convert to AuthResponse entity
AuthResponse toEntity() {
return AuthResponse(
accessToken: accessToken,
user: user,
);
}
}

View File

@@ -0,0 +1,18 @@
/// Login request Data Transfer Object
class LoginDto {
final String email;
final String password;
const LoginDto({
required this.email,
required this.password,
});
/// Convert to JSON for API request
Map<String, dynamic> toJson() {
return {
'email': email,
'password': password,
};
}
}

View File

@@ -0,0 +1,24 @@
/// Register request Data Transfer Object
class RegisterDto {
final String name;
final String email;
final String password;
final List<String> roles;
const RegisterDto({
required this.name,
required this.email,
required this.password,
this.roles = const ['user'],
});
/// Convert to JSON for API request
Map<String, dynamic> toJson() {
return {
'name': name,
'email': email,
'password': password,
'roles': roles,
};
}
}

View File

@@ -0,0 +1,66 @@
import '../../domain/entities/user.dart';
/// User model for data layer (extends User entity)
class UserModel extends User {
const UserModel({
required super.id,
required super.name,
required super.email,
required super.roles,
required super.isActive,
required super.createdAt,
required super.updatedAt,
});
/// Create UserModel from JSON
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
roles: (json['roles'] as List<dynamic>).cast<String>(),
isActive: json['isActive'] as bool? ?? true,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
}
/// Convert UserModel to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'roles': roles,
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
/// Create UserModel from User entity
factory UserModel.fromEntity(User user) {
return UserModel(
id: user.id,
name: user.name,
email: user.email,
roles: user.roles,
isActive: user.isActive,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
);
}
/// Convert to User entity
User toEntity() {
return User(
id: id,
name: name,
email: email,
roles: roles,
isActive: isActive,
createdAt: createdAt,
updatedAt: updatedAt,
);
}
}

View File

@@ -0,0 +1,175 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/storage/secure_storage.dart';
import '../../domain/entities/auth_response.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_datasource.dart';
import '../models/login_dto.dart';
import '../models/register_dto.dart';
/// Implementation of AuthRepository
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final SecureStorage secureStorage;
final DioClient dioClient;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.secureStorage,
required this.dioClient,
});
@override
Future<Either<Failure, AuthResponse>> login({
required String email,
required String password,
}) async {
try {
final loginDto = LoginDto(email: email, password: password);
final authResponse = await remoteDataSource.login(loginDto);
// Save token to secure storage
await secureStorage.saveAccessToken(authResponse.accessToken);
// Set token in Dio client for subsequent requests
dioClient.setAuthToken(authResponse.accessToken);
return Right(authResponse);
} on InvalidCredentialsException catch (e) {
return Left(InvalidCredentialsFailure(e.message));
} on UnauthorizedException catch (e) {
return Left(UnauthorizedFailure(e.message));
} on ValidationException catch (e) {
return Left(ValidationFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
}
@override
Future<Either<Failure, AuthResponse>> register({
required String name,
required String email,
required String password,
List<String> roles = const ['user'],
}) async {
try {
final registerDto = RegisterDto(
name: name,
email: email,
password: password,
roles: roles,
);
final authResponse = await remoteDataSource.register(registerDto);
// Save token to secure storage
await secureStorage.saveAccessToken(authResponse.accessToken);
// Set token in Dio client for subsequent requests
dioClient.setAuthToken(authResponse.accessToken);
return Right(authResponse);
} on ConflictException catch (e) {
return Left(ConflictFailure(e.message));
} on ValidationException catch (e) {
return Left(ValidationFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
}
@override
Future<Either<Failure, User>> getProfile() async {
try {
final user = await remoteDataSource.getProfile();
return Right(user);
} on UnauthorizedException catch (e) {
return Left(UnauthorizedFailure(e.message));
} on TokenExpiredException catch (e) {
return Left(TokenExpiredFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
}
@override
Future<Either<Failure, AuthResponse>> refreshToken() async {
try {
final authResponse = await remoteDataSource.refreshToken();
// Update token in secure storage
await secureStorage.saveAccessToken(authResponse.accessToken);
// Update token in Dio client
dioClient.setAuthToken(authResponse.accessToken);
return Right(authResponse);
} on UnauthorizedException catch (e) {
return Left(UnauthorizedFailure(e.message));
} on TokenExpiredException catch (e) {
return Left(TokenExpiredFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
// Clear token from secure storage
await secureStorage.deleteAllTokens();
// Clear token from Dio client
dioClient.clearAuthToken();
return const Right(null);
} catch (e) {
return Left(CacheFailure('Failed to logout: $e'));
}
}
@override
Future<bool> isAuthenticated() async {
try {
final hasToken = await secureStorage.hasAccessToken();
if (hasToken) {
final token = await secureStorage.getAccessToken();
if (token != null) {
dioClient.setAuthToken(token);
return true;
}
}
return false;
} catch (e) {
return false;
}
}
@override
Future<String?> getAccessToken() async {
try {
return await secureStorage.getAccessToken();
} catch (e) {
return null;
}
}
}