update api
This commit is contained in:
472
lib/features/auth/README.md
Normal file
472
lib/features/auth/README.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Authentication Feature
|
||||
|
||||
Complete JWT-based authentication system for the Retail POS app.
|
||||
|
||||
## Overview
|
||||
|
||||
This feature implements a production-ready authentication system with:
|
||||
- User login with email/password
|
||||
- User registration
|
||||
- JWT token management with secure storage
|
||||
- Automatic token injection in API requests
|
||||
- Profile management
|
||||
- Token refresh capability
|
||||
- Proper error handling
|
||||
|
||||
## Architecture
|
||||
|
||||
The authentication feature follows Clean Architecture principles:
|
||||
|
||||
```
|
||||
auth/
|
||||
├── domain/ # Business logic layer
|
||||
│ ├── entities/ # Core business objects
|
||||
│ │ ├── user.dart
|
||||
│ │ └── auth_response.dart
|
||||
│ └── repositories/ # Repository interfaces
|
||||
│ └── auth_repository.dart
|
||||
│
|
||||
├── data/ # Data layer
|
||||
│ ├── models/ # Data transfer objects and models
|
||||
│ │ ├── login_dto.dart
|
||||
│ │ ├── register_dto.dart
|
||||
│ │ ├── user_model.dart
|
||||
│ │ └── auth_response_model.dart
|
||||
│ ├── datasources/ # Remote data sources
|
||||
│ │ └── auth_remote_datasource.dart
|
||||
│ └── repositories/ # Repository implementations
|
||||
│ └── auth_repository_impl.dart
|
||||
│
|
||||
└── presentation/ # UI layer
|
||||
├── providers/ # Riverpod state management
|
||||
│ └── auth_provider.dart
|
||||
├── pages/ # UI screens
|
||||
│ ├── login_page.dart
|
||||
│ └── register_page.dart
|
||||
└── widgets/ # Reusable UI components
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Base URL: `http://localhost:3000/api`
|
||||
|
||||
### 1. Login
|
||||
```
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
Request:
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "Password123!"
|
||||
}
|
||||
|
||||
Response (200):
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"roles": ["user"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register
|
||||
```
|
||||
POST /auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
Request:
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"password": "Password123!",
|
||||
"roles": ["user"]
|
||||
}
|
||||
|
||||
Response (201):
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Get Profile
|
||||
```
|
||||
GET /auth/profile
|
||||
Authorization: Bearer {token}
|
||||
|
||||
Response (200):
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"roles": ["user"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Refresh Token
|
||||
```
|
||||
POST /auth/refresh
|
||||
Authorization: Bearer {token}
|
||||
|
||||
Response (200):
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Setup (Already configured in injection_container.dart)
|
||||
|
||||
The authentication system is registered in the dependency injection container:
|
||||
|
||||
```dart
|
||||
// In lib/core/di/injection_container.dart
|
||||
Future<void> initDependencies() async {
|
||||
// Secure Storage
|
||||
sl.registerLazySingleton<SecureStorage>(() => SecureStorage());
|
||||
|
||||
// Auth Remote Data Source
|
||||
sl.registerLazySingleton<AuthRemoteDataSource>(
|
||||
() => AuthRemoteDataSourceImpl(dioClient: sl()),
|
||||
);
|
||||
|
||||
// Auth Repository
|
||||
sl.registerLazySingleton<AuthRepository>(
|
||||
() => AuthRepositoryImpl(
|
||||
remoteDataSource: sl(),
|
||||
secureStorage: sl(),
|
||||
dioClient: sl(),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Login Flow
|
||||
|
||||
```dart
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:retail/features/auth/presentation/providers/auth_provider.dart';
|
||||
|
||||
// In a widget
|
||||
class MyWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final success = await ref.read(authProvider.notifier).login(
|
||||
email: 'user@example.com',
|
||||
password: 'Password123!',
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Navigate to home
|
||||
} else {
|
||||
// Show error
|
||||
final error = ref.read(authProvider).errorMessage;
|
||||
print('Login failed: $error');
|
||||
}
|
||||
},
|
||||
child: Text('Login'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check Authentication Status
|
||||
|
||||
```dart
|
||||
// Check if user is authenticated
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
|
||||
// Get current user
|
||||
final user = ref.watch(currentUserProvider);
|
||||
|
||||
if (user != null) {
|
||||
print('Welcome ${user.name}!');
|
||||
print('Roles: ${user.roles}');
|
||||
print('Is Admin: ${user.isAdmin}');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Logout
|
||||
|
||||
```dart
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
```
|
||||
|
||||
### 5. Register New User
|
||||
|
||||
```dart
|
||||
final success = await ref.read(authProvider.notifier).register(
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'Password123!',
|
||||
roles: ['user'], // Optional, defaults to ['user']
|
||||
);
|
||||
```
|
||||
|
||||
### 6. Refresh Token
|
||||
|
||||
```dart
|
||||
final success = await ref.read(authProvider.notifier).refreshToken();
|
||||
|
||||
if (!success) {
|
||||
// Token refresh failed, user logged out automatically
|
||||
}
|
||||
```
|
||||
|
||||
## Bearer Token Injection
|
||||
|
||||
The authentication system automatically injects the JWT bearer token into all API requests:
|
||||
|
||||
### How it Works
|
||||
|
||||
1. **On Login/Register**: Token is saved to secure storage and set in DioClient
|
||||
2. **Automatic Injection**: DioClient interceptor adds `Authorization: Bearer {token}` header to all requests
|
||||
3. **On Logout**: Token is cleared from secure storage and DioClient
|
||||
4. **On App Start**: Token is loaded from secure storage and set in DioClient if valid
|
||||
|
||||
### Implementation Details
|
||||
|
||||
```dart
|
||||
// In lib/core/network/dio_client.dart
|
||||
class DioClient {
|
||||
String? _authToken;
|
||||
|
||||
DioClient() {
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (_authToken != null) {
|
||||
options.headers['Authorization'] = 'Bearer $_authToken';
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setAuthToken(String token) {
|
||||
_authToken = token;
|
||||
}
|
||||
|
||||
void clearAuthToken() {
|
||||
_authToken = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Token Management (Advanced)
|
||||
|
||||
If you need to manually manage tokens:
|
||||
|
||||
```dart
|
||||
// Get token from storage
|
||||
final token = await sl<SecureStorage>().getAccessToken();
|
||||
|
||||
// Set token in DioClient
|
||||
sl<DioClient>().setAuthToken(token!);
|
||||
|
||||
// Clear token
|
||||
sl<DioClient>().clearAuthToken();
|
||||
await sl<SecureStorage>().deleteAllTokens();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The authentication system handles various error scenarios:
|
||||
|
||||
### Error Types
|
||||
|
||||
1. **InvalidCredentialsFailure**: Wrong email or password
|
||||
2. **ConflictFailure**: Email already exists (registration)
|
||||
3. **UnauthorizedFailure**: Invalid or expired token
|
||||
4. **NetworkFailure**: No internet connection
|
||||
5. **ServerFailure**: Server errors (500, 503, etc.)
|
||||
6. **ValidationFailure**: Invalid input data
|
||||
|
||||
### Error Handling Example
|
||||
|
||||
```dart
|
||||
final success = await ref.read(authProvider.notifier).login(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
final error = ref.read(authProvider).errorMessage;
|
||||
|
||||
// Show user-friendly error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error ?? 'Login failed'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Secure Storage
|
||||
|
||||
JWT tokens are stored securely using `flutter_secure_storage`:
|
||||
|
||||
- **iOS**: Keychain
|
||||
- **Android**: EncryptedSharedPreferences
|
||||
- **Web**: Web Crypto API
|
||||
- **Desktop**: Platform-specific secure storage
|
||||
|
||||
```dart
|
||||
// Access secure storage
|
||||
final secureStorage = sl<SecureStorage>();
|
||||
|
||||
// Save token
|
||||
await secureStorage.saveAccessToken(token);
|
||||
|
||||
// Get token
|
||||
final token = await secureStorage.getAccessToken();
|
||||
|
||||
// Check if token exists
|
||||
final hasToken = await secureStorage.hasAccessToken();
|
||||
|
||||
// Delete token
|
||||
await secureStorage.deleteAllTokens();
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
The authentication state is managed using Riverpod 3.0:
|
||||
|
||||
### AuthState
|
||||
|
||||
```dart
|
||||
class AuthState {
|
||||
final User? user; // Current authenticated user
|
||||
final bool isAuthenticated; // Authentication status
|
||||
final bool isLoading; // Loading state
|
||||
final String? errorMessage; // Error message (if any)
|
||||
}
|
||||
```
|
||||
|
||||
### Watching Auth State
|
||||
|
||||
```dart
|
||||
// Watch entire auth state
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
// Access specific properties
|
||||
final user = authState.user;
|
||||
final isLoading = authState.isLoading;
|
||||
final error = authState.errorMessage;
|
||||
|
||||
// Or use convenience providers
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
```
|
||||
|
||||
## Protected Routes
|
||||
|
||||
Implement route guards to protect authenticated routes:
|
||||
|
||||
```dart
|
||||
class AuthGuard extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const AuthGuard({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
final isLoading = ref.watch(authProvider.select((s) => s.isLoading));
|
||||
|
||||
if (isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return const LoginPage();
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
MaterialApp(
|
||||
home: AuthGuard(
|
||||
child: HomePage(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```dart
|
||||
test('login should return success with valid credentials', () async {
|
||||
// Arrange
|
||||
final repository = MockAuthRepository();
|
||||
when(repository.login(email: any, password: any))
|
||||
.thenAnswer((_) async => Right(mockAuthResponse));
|
||||
|
||||
// Act
|
||||
final result = await repository.login(
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!',
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Test the complete authentication flow from UI to API.
|
||||
|
||||
## Code Generation
|
||||
|
||||
Run code generation for Riverpod providers:
|
||||
|
||||
```bash
|
||||
# Generate auth_provider.g.dart
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Watch for changes
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
|
||||
## Production Considerations
|
||||
|
||||
1. **HTTPS**: Always use HTTPS in production
|
||||
2. **Token Expiry**: Implement automatic token refresh on 401 errors
|
||||
3. **Biometric Auth**: Add fingerprint/face ID support
|
||||
4. **Password Strength**: Enforce strong password requirements
|
||||
5. **Rate Limiting**: Handle rate limiting (429 errors)
|
||||
6. **Secure Storage**: Tokens are already stored securely
|
||||
7. **Session Management**: Clear tokens on app uninstall
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Biometric authentication (fingerprint, face ID)
|
||||
- [ ] Remember me functionality
|
||||
- [ ] Password reset flow
|
||||
- [ ] Email verification
|
||||
- [ ] Social login (Google, Apple, Facebook)
|
||||
- [ ] Multi-factor authentication (MFA)
|
||||
- [ ] Session timeout warning
|
||||
- [ ] Device management
|
||||
159
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
159
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/features/auth/data/models/auth_response_model.dart
Normal file
42
lib/features/auth/data/models/auth_response_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/features/auth/data/models/login_dto.dart
Normal file
18
lib/features/auth/data/models/login_dto.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
24
lib/features/auth/data/models/register_dto.dart
Normal file
24
lib/features/auth/data/models/register_dto.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
66
lib/features/auth/data/models/user_model.dart
Normal file
66
lib/features/auth/data/models/user_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
175
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
175
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/features/auth/domain/entities/auth_response.dart
Normal file
16
lib/features/auth/domain/entities/auth_response.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user.dart';
|
||||
|
||||
/// Authentication response entity
|
||||
class AuthResponse extends Equatable {
|
||||
final String accessToken;
|
||||
final User user;
|
||||
|
||||
const AuthResponse({
|
||||
required this.accessToken,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [accessToken, user];
|
||||
}
|
||||
45
lib/features/auth/domain/entities/user.dart
Normal file
45
lib/features/auth/domain/entities/user.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// User entity representing a user in the system
|
||||
class User extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
final List<String> roles;
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.roles,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
roles,
|
||||
isActive,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
|
||||
/// Check if user has a specific role
|
||||
bool hasRole(String role) => roles.contains(role);
|
||||
|
||||
/// Check if user is admin
|
||||
bool get isAdmin => hasRole('admin');
|
||||
|
||||
/// Check if user is manager
|
||||
bool get isManager => hasRole('manager');
|
||||
|
||||
/// Check if user is cashier
|
||||
bool get isCashier => hasRole('cashier');
|
||||
}
|
||||
36
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
36
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/auth_response.dart';
|
||||
import '../entities/user.dart';
|
||||
|
||||
/// Abstract repository for authentication operations
|
||||
abstract class AuthRepository {
|
||||
/// Login user with email and password
|
||||
Future<Either<Failure, AuthResponse>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
/// Register new user
|
||||
Future<Either<Failure, AuthResponse>> register({
|
||||
required String name,
|
||||
required String email,
|
||||
required String password,
|
||||
List<String> roles = const ['user'],
|
||||
});
|
||||
|
||||
/// Get current user profile
|
||||
Future<Either<Failure, User>> getProfile();
|
||||
|
||||
/// Refresh access token
|
||||
Future<Either<Failure, AuthResponse>> refreshToken();
|
||||
|
||||
/// Logout user (clear local token)
|
||||
Future<Either<Failure, void>> logout();
|
||||
|
||||
/// Check if user is authenticated
|
||||
Future<bool> isAuthenticated();
|
||||
|
||||
/// Get stored access token
|
||||
Future<String?> getAccessToken();
|
||||
}
|
||||
488
lib/features/auth/example_usage.dart
Normal file
488
lib/features/auth/example_usage.dart
Normal file
@@ -0,0 +1,488 @@
|
||||
// ignore_for_file: unused_local_variable, avoid_print
|
||||
|
||||
/// Example usage of the authentication system
|
||||
///
|
||||
/// This file demonstrates how to use the authentication feature
|
||||
/// in your Flutter app. Copy the patterns shown here into your
|
||||
/// actual implementation.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'presentation/providers/auth_provider.dart';
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 1: Login Flow
|
||||
// ============================================================================
|
||||
|
||||
class LoginExample extends ConsumerWidget {
|
||||
final TextEditingController emailController;
|
||||
final TextEditingController passwordController;
|
||||
|
||||
const LoginExample({
|
||||
super.key,
|
||||
required this.emailController,
|
||||
required this.passwordController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Watch auth state
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Show loading indicator
|
||||
if (authState.isLoading) const CircularProgressIndicator(),
|
||||
|
||||
// Login button
|
||||
ElevatedButton(
|
||||
onPressed: authState.isLoading
|
||||
? null
|
||||
: () async {
|
||||
// Call login
|
||||
final success = await ref.read(authProvider.notifier).login(
|
||||
email: emailController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
|
||||
// Handle result
|
||||
if (success) {
|
||||
// Login successful - navigate to home
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
}
|
||||
} else {
|
||||
// Login failed - show error
|
||||
final error = ref.read(authProvider).errorMessage;
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error ?? 'Login failed'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 2: Register Flow
|
||||
// ============================================================================
|
||||
|
||||
class RegisterExample extends ConsumerStatefulWidget {
|
||||
const RegisterExample({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<RegisterExample> createState() => _RegisterExampleState();
|
||||
}
|
||||
|
||||
class _RegisterExampleState extends ConsumerState<RegisterExample> {
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleRegister() async {
|
||||
final success = await ref.read(authProvider.notifier).register(
|
||||
name: _nameController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
roles: ['user'], // Optional, defaults to ['user']
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
// Registration successful
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
} else {
|
||||
// Registration failed
|
||||
final error = ref.read(authProvider).errorMessage;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error ?? 'Registration failed'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: authState.isLoading ? null : _handleRegister,
|
||||
child: const Text('Register'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 3: Check Authentication Status
|
||||
// ============================================================================
|
||||
|
||||
class AuthStatusExample extends ConsumerWidget {
|
||||
const AuthStatusExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Method 1: Watch entire auth state
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
// Method 2: Use convenience providers
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Check if authenticated
|
||||
if (authState.isAuthenticated) ...[
|
||||
Text('Welcome ${authState.user?.name}!'),
|
||||
Text('Email: ${authState.user?.email}'),
|
||||
Text('Roles: ${authState.user?.roles.join(", ")}'),
|
||||
|
||||
// Check user roles
|
||||
if (currentUser?.isAdmin ?? false)
|
||||
const Text('You are an admin'),
|
||||
if (currentUser?.isManager ?? false)
|
||||
const Text('You are a manager'),
|
||||
if (currentUser?.isCashier ?? false)
|
||||
const Text('You are a cashier'),
|
||||
] else ...[
|
||||
const Text('Not authenticated'),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 4: Protected Route Guard
|
||||
// ============================================================================
|
||||
|
||||
class AuthGuard extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const AuthGuard({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
final isLoading = ref.watch(authProvider.select((s) => s.isLoading));
|
||||
|
||||
// Show loading while checking auth status
|
||||
if (isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
// If not authenticated, show login page
|
||||
if (!isAuthenticated) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('Please login to continue'),
|
||||
),
|
||||
);
|
||||
// In real app: return const LoginPage();
|
||||
}
|
||||
|
||||
// User is authenticated, show protected content
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in main app:
|
||||
// MaterialApp(
|
||||
// home: AuthGuard(
|
||||
// child: HomePage(),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 5: Logout
|
||||
// ============================================================================
|
||||
|
||||
class LogoutExample extends ConsumerWidget {
|
||||
const LogoutExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
// Show confirmation dialog
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Logout'),
|
||||
content: const Text('Are you sure you want to logout?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Logout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
// Perform logout
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
|
||||
// Navigate to login
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Logout'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 6: Get Profile (Refresh User Data)
|
||||
// ============================================================================
|
||||
|
||||
class ProfileExample extends ConsumerWidget {
|
||||
const ProfileExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (currentUser != null) ...[
|
||||
Text('Name: ${currentUser.name}'),
|
||||
Text('Email: ${currentUser.email}'),
|
||||
Text('Roles: ${currentUser.roles.join(", ")}'),
|
||||
],
|
||||
|
||||
// Refresh profile button
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).getProfile();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Profile refreshed')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Refresh Profile'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 7: Refresh Token
|
||||
// ============================================================================
|
||||
|
||||
class RefreshTokenExample extends ConsumerWidget {
|
||||
const RefreshTokenExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final success = await ref.read(authProvider.notifier).refreshToken();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Token refreshed successfully')),
|
||||
);
|
||||
} else {
|
||||
// Token refresh failed - user was logged out
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Session expired. Please login again.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
}
|
||||
},
|
||||
child: const Text('Refresh Token'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 8: Role-Based UI
|
||||
// ============================================================================
|
||||
|
||||
class RoleBasedUIExample extends ConsumerWidget {
|
||||
const RoleBasedUIExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Show to all authenticated users
|
||||
const Text('Dashboard'),
|
||||
|
||||
// Show only to admins
|
||||
if (currentUser?.isAdmin ?? false) ...[
|
||||
const Text('Admin Panel'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to admin panel
|
||||
},
|
||||
child: const Text('Manage Users'),
|
||||
),
|
||||
],
|
||||
|
||||
// Show only to managers
|
||||
if (currentUser?.isManager ?? false) ...[
|
||||
const Text('Manager Tools'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to manager tools
|
||||
},
|
||||
child: const Text('View Reports'),
|
||||
),
|
||||
],
|
||||
|
||||
// Show only to cashiers
|
||||
if (currentUser?.isCashier ?? false) ...[
|
||||
const Text('POS Terminal'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to POS
|
||||
},
|
||||
child: const Text('Start Transaction'),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 9: Error Handling
|
||||
// ============================================================================
|
||||
|
||||
class ErrorHandlingExample extends ConsumerWidget {
|
||||
const ErrorHandlingExample({super.key});
|
||||
|
||||
Future<void> _handleLogin(BuildContext context, WidgetRef ref) async {
|
||||
final success = await ref.read(authProvider.notifier).login(
|
||||
email: 'test@example.com',
|
||||
password: 'password',
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (!success) {
|
||||
final error = ref.read(authProvider).errorMessage;
|
||||
|
||||
// Different error messages result in different UI feedback
|
||||
String userMessage;
|
||||
Color backgroundColor;
|
||||
|
||||
if (error?.contains('Invalid email or password') ?? false) {
|
||||
userMessage = 'Incorrect email or password. Please try again.';
|
||||
backgroundColor = Colors.red;
|
||||
} else if (error?.contains('Network') ?? false) {
|
||||
userMessage = 'No internet connection. Please check your network.';
|
||||
backgroundColor = Colors.orange;
|
||||
} else if (error?.contains('Server') ?? false) {
|
||||
userMessage = 'Server error. Please try again later.';
|
||||
backgroundColor = Colors.red[700]!;
|
||||
} else {
|
||||
userMessage = error ?? 'Login failed. Please try again.';
|
||||
backgroundColor = Colors.red;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(userMessage),
|
||||
backgroundColor: backgroundColor,
|
||||
action: SnackBarAction(
|
||||
label: 'Retry',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => _handleLogin(context, ref),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => _handleLogin(context, ref),
|
||||
child: const Text('Login with Error Handling'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 10: Using Auth in Non-Widget Code
|
||||
// ============================================================================
|
||||
|
||||
void nonWidgetExample() {
|
||||
// If you need to access auth outside widgets (e.g., in services),
|
||||
// use the service locator directly:
|
||||
|
||||
// import 'package:retail/core/di/injection_container.dart';
|
||||
// import 'package:retail/features/auth/domain/repositories/auth_repository.dart';
|
||||
|
||||
// final authRepository = sl<AuthRepository>();
|
||||
//
|
||||
// // Check if authenticated
|
||||
// final isAuthenticated = await authRepository.isAuthenticated();
|
||||
//
|
||||
// // Get token
|
||||
// final token = await authRepository.getAccessToken();
|
||||
//
|
||||
// print('Token: $token');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXAMPLE 11: Automatic Token Injection Test
|
||||
// ============================================================================
|
||||
|
||||
void tokenInjectionExample() {
|
||||
// Once logged in, all API requests automatically include the JWT token:
|
||||
//
|
||||
// The DioClient interceptor adds this header to all requests:
|
||||
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
//
|
||||
// You don't need to manually add the token - it's automatic!
|
||||
|
||||
// Example of making an API call after login:
|
||||
// final response = await sl<DioClient>().get('/api/products');
|
||||
//
|
||||
// The above request will automatically include:
|
||||
// Headers: {
|
||||
// "Authorization": "Bearer <your-jwt-token>",
|
||||
// "Content-Type": "application/json",
|
||||
// "Accept": "application/json"
|
||||
// }
|
||||
}
|
||||
171
lib/features/auth/presentation/pages/login_page.dart
Normal file
171
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// Login page for user authentication
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final success = await ref.read(authProvider.notifier).login(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
// Navigate to home or show success
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Login successful!')),
|
||||
);
|
||||
// TODO: Navigate to home page
|
||||
} else {
|
||||
final errorMessage = ref.read(authProvider).errorMessage;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorMessage ?? 'Login failed'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Login'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo or app name
|
||||
Icon(
|
||||
Icons.shopping_cart,
|
||||
size: 80,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Retail POS',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: Icon(Icons.email),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Please enter a valid email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Login button
|
||||
FilledButton(
|
||||
onPressed: authState.isLoading ? null : _handleLogin,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: authState.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text('Login'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Register link
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Navigate to register page
|
||||
// Navigator.push(context, MaterialPageRoute(builder: (_) => const RegisterPage()));
|
||||
},
|
||||
child: const Text('Don\'t have an account? Register'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
234
lib/features/auth/presentation/pages/register_page.dart
Normal file
234
lib/features/auth/presentation/pages/register_page.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// Register page for new user registration
|
||||
class RegisterPage extends ConsumerStatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleRegister() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final success = await ref.read(authProvider.notifier).register(
|
||||
name: _nameController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
// Navigate to home or show success
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Registration successful!')),
|
||||
);
|
||||
// TODO: Navigate to home page
|
||||
} else {
|
||||
final errorMessage = ref.read(authProvider).errorMessage;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorMessage ?? 'Registration failed'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Register'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Logo or app name
|
||||
Icon(
|
||||
Icons.shopping_cart,
|
||||
size: 80,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Create Account',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Name field
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Full Name',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your name';
|
||||
}
|
||||
if (value.length < 2) {
|
||||
return 'Name must be at least 2 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: Icon(Icons.email),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Please enter a valid email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
}
|
||||
// Check for uppercase, lowercase, and number
|
||||
if (!RegExp(r'(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
|
||||
return 'Password must contain uppercase, lowercase, and number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Confirm password field
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _obscureConfirmPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Confirm Password',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirmPassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureConfirmPassword = !_obscureConfirmPassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please confirm your password';
|
||||
}
|
||||
if (value != _passwordController.text) {
|
||||
return 'Passwords do not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Register button
|
||||
FilledButton(
|
||||
onPressed: authState.isLoading ? null : _handleRegister,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: authState.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text('Register'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Login link
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Already have an account? Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
215
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
215
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
|
||||
part 'auth_provider.g.dart';
|
||||
|
||||
/// Provider for AuthRepository
|
||||
@riverpod
|
||||
AuthRepository authRepository(Ref ref) {
|
||||
return sl<AuthRepository>();
|
||||
}
|
||||
|
||||
/// Auth state class
|
||||
class AuthState {
|
||||
final User? user;
|
||||
final bool isAuthenticated;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const AuthState({
|
||||
this.user,
|
||||
this.isAuthenticated = false,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
AuthState copyWith({
|
||||
User? user,
|
||||
bool? isAuthenticated,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: user ?? this.user,
|
||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Auth state notifier provider
|
||||
@riverpod
|
||||
class Auth extends _$Auth {
|
||||
@override
|
||||
AuthState build() {
|
||||
_checkAuthStatus();
|
||||
return const AuthState();
|
||||
}
|
||||
|
||||
AuthRepository get _repository => ref.read(authRepositoryProvider);
|
||||
|
||||
/// Check if user is authenticated on app start
|
||||
Future<void> _checkAuthStatus() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
|
||||
final isAuthenticated = await _repository.isAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
// Get user profile
|
||||
final result = await _repository.getProfile();
|
||||
result.fold(
|
||||
(failure) {
|
||||
state = const AuthState(
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
);
|
||||
},
|
||||
(user) {
|
||||
state = AuthState(
|
||||
user: user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
state = const AuthState(
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Login user
|
||||
Future<bool> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
final result = await _repository.login(email: email, password: password);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message,
|
||||
);
|
||||
return false;
|
||||
},
|
||||
(authResponse) {
|
||||
state = AuthState(
|
||||
user: authResponse.user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Register new user
|
||||
Future<bool> register({
|
||||
required String name,
|
||||
required String email,
|
||||
required String password,
|
||||
List<String> roles = const ['user'],
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
final result = await _repository.register(
|
||||
name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
roles: roles,
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message,
|
||||
);
|
||||
return false;
|
||||
},
|
||||
(authResponse) {
|
||||
state = AuthState(
|
||||
user: authResponse.user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Get user profile (refresh user data)
|
||||
Future<void> getProfile() async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
final result = await _repository.getProfile();
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message,
|
||||
);
|
||||
},
|
||||
(user) {
|
||||
state = state.copyWith(
|
||||
user: user,
|
||||
isLoading: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh access token
|
||||
Future<bool> refreshToken() async {
|
||||
final result = await _repository.refreshToken();
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
// If token refresh fails, logout user
|
||||
logout();
|
||||
return false;
|
||||
},
|
||||
(authResponse) {
|
||||
state = state.copyWith(user: authResponse.user);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Logout user
|
||||
Future<void> logout() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
|
||||
await _repository.logout();
|
||||
|
||||
state = const AuthState(
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Current authenticated user provider
|
||||
@riverpod
|
||||
User? currentUser(Ref ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
return authState.user;
|
||||
}
|
||||
|
||||
/// Is authenticated provider
|
||||
@riverpod
|
||||
bool isAuthenticated(Ref ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
return authState.isAuthenticated;
|
||||
}
|
||||
204
lib/features/auth/presentation/providers/auth_provider.g.dart
Normal file
204
lib/features/auth/presentation/providers/auth_provider.g.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for AuthRepository
|
||||
|
||||
@ProviderFor(authRepository)
|
||||
const authRepositoryProvider = AuthRepositoryProvider._();
|
||||
|
||||
/// Provider for AuthRepository
|
||||
|
||||
final class AuthRepositoryProvider
|
||||
extends $FunctionalProvider<AuthRepository, AuthRepository, AuthRepository>
|
||||
with $Provider<AuthRepository> {
|
||||
/// Provider for AuthRepository
|
||||
const AuthRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AuthRepository> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AuthRepository create(Ref ref) {
|
||||
return authRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authRepositoryHash() => r'0483b13ac95333b56a1a82f6c9fdb64ae46f287d';
|
||||
|
||||
/// Auth state notifier provider
|
||||
|
||||
@ProviderFor(Auth)
|
||||
const authProvider = AuthProvider._();
|
||||
|
||||
/// Auth state notifier provider
|
||||
final class AuthProvider extends $NotifierProvider<Auth, AuthState> {
|
||||
/// Auth state notifier provider
|
||||
const AuthProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Auth create() => Auth();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authHash() => r'c88e150224fa855ed0ddfba30bef9e2b289f329d';
|
||||
|
||||
/// Auth state notifier provider
|
||||
|
||||
abstract class _$Auth extends $Notifier<AuthState> {
|
||||
AuthState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AuthState, AuthState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AuthState, AuthState>,
|
||||
AuthState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Current authenticated user provider
|
||||
|
||||
@ProviderFor(currentUser)
|
||||
const currentUserProvider = CurrentUserProvider._();
|
||||
|
||||
/// Current authenticated user provider
|
||||
|
||||
final class CurrentUserProvider extends $FunctionalProvider<User?, User?, User?>
|
||||
with $Provider<User?> {
|
||||
/// Current authenticated user provider
|
||||
const CurrentUserProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'currentUserProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$currentUserHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<User?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
User? create(Ref ref) {
|
||||
return currentUser(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(User? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<User?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$currentUserHash() => r'4c8cb60cef35a4fd001291434558037d6c85faf5';
|
||||
|
||||
/// Is authenticated provider
|
||||
|
||||
@ProviderFor(isAuthenticated)
|
||||
const isAuthenticatedProvider = IsAuthenticatedProvider._();
|
||||
|
||||
/// Is authenticated provider
|
||||
|
||||
final class IsAuthenticatedProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Is authenticated provider
|
||||
const IsAuthenticatedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isAuthenticatedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isAuthenticatedHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isAuthenticated(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isAuthenticatedHash() => r'003f7e85bfa5ae774792659ce771b5b59ebf04f8';
|
||||
Reference in New Issue
Block a user