Files
retail/lib/features/auth
Phuoc Nguyen bdaf0b96c5 fix
2025-10-10 17:36:10 +07:00
..
fix
2025-10-10 17:36:10 +07:00
fix
2025-10-10 17:36:10 +07:00
fix
2025-10-10 17:36:10 +07:00
fix
2025-10-10 17:36:10 +07:00
2025-10-10 17:15:40 +07:00
2025-10-10 17:15:40 +07:00

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:

// 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

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

// 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

await ref.read(authProvider.notifier).logout();

5. Register New User

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

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

// 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:

// 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

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
// 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

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

// 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:

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

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:

# 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