- Create auth feature with Clean Architecture (domain/data/presentation) - Add login and register pages with form validation - Implement secure token storage with flutter_secure_storage - Create AuthenticatedClient for automatic Bearer token headers - Add AuthViewModel for global auth state management - Update router with auth guards (redirect to login if not authenticated) - Add logout option to settings page - Update TaskRemoteDataSource to use authenticated client - Add auth-related localization strings (EN/DE)
180 lines
4.7 KiB
Dart
180 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../../../../core/errors/exceptions.dart';
|
|
import '../../../../core/logging/app_logger.dart';
|
|
import '../models/token_model.dart';
|
|
import '../models/user_model.dart';
|
|
|
|
abstract class AuthRemoteDataSource {
|
|
Future<UserModel> register({
|
|
required String email,
|
|
required String password,
|
|
required String name,
|
|
});
|
|
|
|
Future<TokenModel> login({
|
|
required String email,
|
|
required String password,
|
|
});
|
|
|
|
Future<UserModel> getCurrentUser(String accessToken);
|
|
|
|
Future<TokenModel> refreshToken(String refreshToken);
|
|
}
|
|
|
|
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|
final AppLogger logger;
|
|
final http.Client _client;
|
|
|
|
static const String _baseUrl = String.fromEnvironment(
|
|
'API_URL',
|
|
defaultValue: 'http://localhost:8000',
|
|
);
|
|
|
|
AuthRemoteDataSourceImpl({
|
|
required this.logger,
|
|
http.Client? client,
|
|
}) : _client = client ?? http.Client();
|
|
|
|
Map<String, String> get _headers => {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
Map<String, String> _authHeaders(String token) => {
|
|
..._headers,
|
|
'Authorization': 'Bearer $token',
|
|
};
|
|
|
|
@override
|
|
Future<UserModel> register({
|
|
required String email,
|
|
required String password,
|
|
required String name,
|
|
}) async {
|
|
final uri = Uri.parse('$_baseUrl/auth/register');
|
|
logger.info('POST $uri');
|
|
|
|
try {
|
|
final response = await _client.post(
|
|
uri,
|
|
headers: _headers,
|
|
body: json.encode({
|
|
'email': email,
|
|
'password': password,
|
|
'name': name,
|
|
}),
|
|
);
|
|
logger.debug('Response: ${response.statusCode}');
|
|
|
|
if (response.statusCode == 201 || response.statusCode == 200) {
|
|
return UserModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 400) {
|
|
final body = json.decode(response.body);
|
|
throw AuthException(
|
|
message: body['detail'] ?? 'Registration failed',
|
|
);
|
|
} else {
|
|
throw ServerException(
|
|
message: 'Registration failed',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on SocketException {
|
|
throw const NetworkException();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<TokenModel> login({
|
|
required String email,
|
|
required String password,
|
|
}) async {
|
|
final uri = Uri.parse('$_baseUrl/auth/login');
|
|
logger.info('POST $uri');
|
|
|
|
try {
|
|
final response = await _client.post(
|
|
uri,
|
|
headers: _headers,
|
|
body: json.encode({
|
|
'email': email,
|
|
'password': password,
|
|
}),
|
|
);
|
|
logger.debug('Response: ${response.statusCode}');
|
|
|
|
if (response.statusCode == 200) {
|
|
return TokenModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw const AuthException(message: 'Invalid email or password');
|
|
} else {
|
|
throw ServerException(
|
|
message: 'Login failed',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on SocketException {
|
|
throw const NetworkException();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<UserModel> getCurrentUser(String accessToken) async {
|
|
final uri = Uri.parse('$_baseUrl/auth/me');
|
|
logger.info('GET $uri');
|
|
|
|
try {
|
|
final response = await _client.get(
|
|
uri,
|
|
headers: _authHeaders(accessToken),
|
|
);
|
|
logger.debug('Response: ${response.statusCode}');
|
|
|
|
if (response.statusCode == 200) {
|
|
return UserModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw const UnauthorizedException();
|
|
} else {
|
|
throw ServerException(
|
|
message: 'Failed to get user info',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on SocketException {
|
|
throw const NetworkException();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<TokenModel> refreshToken(String refreshToken) async {
|
|
final uri = Uri.parse('$_baseUrl/auth/refresh');
|
|
logger.info('POST $uri');
|
|
|
|
try {
|
|
final response = await _client.post(
|
|
uri,
|
|
headers: _headers,
|
|
body: json.encode({'refresh_token': refreshToken}),
|
|
);
|
|
logger.debug('Response: ${response.statusCode}');
|
|
|
|
if (response.statusCode == 200) {
|
|
return TokenModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw const UnauthorizedException(message: 'Session expired');
|
|
} else {
|
|
throw ServerException(
|
|
message: 'Token refresh failed',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on SocketException {
|
|
throw const NetworkException();
|
|
}
|
|
}
|
|
}
|