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
|
||||
Reference in New Issue
Block a user