473 lines
10 KiB
Markdown
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
|