Files
retail/lib/features/auth/README.md
Phuoc Nguyen 04f7042b8d update api
2025-10-10 17:15:40 +07:00

473 lines
10 KiB
Markdown

# 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