236 lines
6.7 KiB
Markdown
236 lines
6.7 KiB
Markdown
|
|
# Flutter Auth Integration Design
|
||
|
|
|
||
|
|
Date: 2026-02-25
|
||
|
|
Status: Approved
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
Integrate Flutter mobile app with backend auth APIs, including signup, login, token management, and route protection.
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
- Signup flow: `/auth/signup/start` + `/auth/signup/verify`
|
||
|
|
- Login flow: `/auth/login`
|
||
|
|
- Token management: access token, refresh token, secure storage
|
||
|
|
- Route protection: redirect unauthenticated users
|
||
|
|
|
||
|
|
Out of scope:
|
||
|
|
- OTP login (removed from UI)
|
||
|
|
- Password reset
|
||
|
|
- Social login
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Directory Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
lib/
|
||
|
|
├── core/
|
||
|
|
│ ├── api/
|
||
|
|
│ │ ├── api_client.dart # dio wrapper
|
||
|
|
│ │ ├── api_interceptor.dart # token injection, refresh logic
|
||
|
|
│ │ └── api_exception.dart # unified error types
|
||
|
|
│ ├── storage/
|
||
|
|
│ │ └── token_storage.dart # flutter_secure_storage wrapper
|
||
|
|
│ └── di/
|
||
|
|
│ └── injection.dart # get_it configuration
|
||
|
|
│
|
||
|
|
├── features/auth/
|
||
|
|
│ ├── data/
|
||
|
|
│ │ ├── models/
|
||
|
|
│ │ │ ├── signup_request.dart
|
||
|
|
│ │ │ ├── login_request.dart
|
||
|
|
│ │ │ └── auth_response.dart
|
||
|
|
│ │ ├── auth_api.dart # API interface
|
||
|
|
│ │ ├── auth_repository.dart # abstract interface
|
||
|
|
│ │ └── auth_repository_impl.dart # implementation
|
||
|
|
│ ├── presentation/
|
||
|
|
│ │ ├── bloc/
|
||
|
|
│ │ │ ├── auth_bloc.dart
|
||
|
|
│ │ │ ├── auth_event.dart
|
||
|
|
│ │ │ └── auth_state.dart
|
||
|
|
│ │ └── cubits/
|
||
|
|
│ │ ├── register_cubit.dart # signup form state
|
||
|
|
│ │ └── login_cubit.dart # login form state
|
||
|
|
│ └── ui/screens/ # existing UI adjustments
|
||
|
|
│
|
||
|
|
└── main.dart # DI initialization
|
||
|
|
```
|
||
|
|
|
||
|
|
### API Layer
|
||
|
|
|
||
|
|
**ApiInterceptor responsibilities**:
|
||
|
|
1. Auto-inject `Authorization: Bearer <access_token>`
|
||
|
|
2. On 401 response, auto-refresh token and retry
|
||
|
|
3. If refresh fails, trigger logout
|
||
|
|
|
||
|
|
**AuthApi interface**:
|
||
|
|
```dart
|
||
|
|
abstract class AuthApi {
|
||
|
|
Future<AuthResponse> signupStart(SignupStartRequest request);
|
||
|
|
Future<AuthResponse> signupVerify(SignupVerifyRequest request);
|
||
|
|
Future<AuthResponse> signupResend(String email);
|
||
|
|
Future<AuthResponse> login(LoginRequest request);
|
||
|
|
Future<AuthResponse> refresh(String refreshToken);
|
||
|
|
Future<void> logout(String refreshToken);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**TokenStorage interface**:
|
||
|
|
```dart
|
||
|
|
abstract class TokenStorage {
|
||
|
|
Future<String?> getAccessToken();
|
||
|
|
Future<String?> getRefreshToken();
|
||
|
|
Future<void> saveTokens({required String access, required String refresh});
|
||
|
|
Future<void> clear();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Error handling**:
|
||
|
|
- `ApiException` wraps network errors, timeouts, 4xx/5xx
|
||
|
|
- UI layer displays user-friendly messages via `ApiException`
|
||
|
|
|
||
|
|
### State Management
|
||
|
|
|
||
|
|
**AuthBloc (global auth state)**:
|
||
|
|
```dart
|
||
|
|
abstract class AuthState {}
|
||
|
|
class AuthInitial extends AuthState {}
|
||
|
|
class AuthAuthenticated extends AuthState {
|
||
|
|
final User user;
|
||
|
|
final String accessToken;
|
||
|
|
}
|
||
|
|
class AuthUnauthenticated extends AuthState {}
|
||
|
|
class AuthLoading extends AuthState {}
|
||
|
|
|
||
|
|
// Events
|
||
|
|
class AuthStarted extends AuthEvent {} // Check token on app start
|
||
|
|
class AuthLoggedIn extends AuthEvent {} // Login success
|
||
|
|
class AuthLoggedOut extends AuthEvent {} // Logout
|
||
|
|
class AuthTokenRefreshed extends AuthEvent {} // Token refreshed
|
||
|
|
```
|
||
|
|
|
||
|
|
**RegisterCubit (signup form)**:
|
||
|
|
```dart
|
||
|
|
class RegisterState {
|
||
|
|
final Username username; // formz input
|
||
|
|
final Email email;
|
||
|
|
final Password password;
|
||
|
|
final FormzSubmissionStatus status;
|
||
|
|
final String? errorMessage;
|
||
|
|
final String? pendingEmail; // stored after signup/start success
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**LoginCubit (login form)**:
|
||
|
|
```dart
|
||
|
|
class LoginState {
|
||
|
|
final Email email;
|
||
|
|
final Password password;
|
||
|
|
final FormzSubmissionStatus status;
|
||
|
|
final String? errorMessage;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### UI Adjustments
|
||
|
|
|
||
|
|
**Signup flow changes**:
|
||
|
|
|
||
|
|
| Original | New |
|
||
|
|
|----------|-----|
|
||
|
|
| Step1: nickname + email | Step1: username + email + password -> call `/signup/start` |
|
||
|
|
| Step2: password + code + invite | Step2: code + invite(kept) -> call `/signup/verify` |
|
||
|
|
|
||
|
|
**Login flow changes**:
|
||
|
|
- Remove "Login with OTP" button
|
||
|
|
- `login_email_screen.dart` -> `login_password_screen.dart` passes email
|
||
|
|
- Password page calls `/login`, navigates to `/home` on success
|
||
|
|
|
||
|
|
**Route protection**:
|
||
|
|
- Use `GoRouter` `redirect` logic
|
||
|
|
- Unauthenticated user accessing protected route -> redirect to `/`
|
||
|
|
- Authenticated user accessing `/login` or `/register` -> redirect to `/home`
|
||
|
|
|
||
|
|
```dart
|
||
|
|
redirect: (context, state) {
|
||
|
|
final isAuthenticated = // check AuthBloc state;
|
||
|
|
final isAuthRoute = state.matchedLocation.startsWith('/login')
|
||
|
|
|| state.matchedLocation.startsWith('/register');
|
||
|
|
|
||
|
|
if (!isAuthenticated && !isAuthRoute) return '/';
|
||
|
|
if (isAuthenticated && isAuthRoute) return '/home';
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Dependency Injection
|
||
|
|
|
||
|
|
**DI configuration**:
|
||
|
|
```dart
|
||
|
|
final sl = GetIt.instance;
|
||
|
|
|
||
|
|
Future<void> configureDependencies() async {
|
||
|
|
// Core
|
||
|
|
sl.registerSingleton<Dio>(Dio(BaseOptions(baseUrl: env.apiUrl)));
|
||
|
|
sl.registerSingleton<TokenStorage>(SecureTokenStorage());
|
||
|
|
sl.registerSingleton<ApiClient>(ApiClient(sl(), sl()));
|
||
|
|
|
||
|
|
// Auth
|
||
|
|
sl.registerSingleton<AuthApi>(AuthApiImpl(sl()));
|
||
|
|
sl.registerSingleton<AuthRepository>(AuthRepositoryImpl(sl(), sl()));
|
||
|
|
sl.registerSingleton<AuthBloc>(AuthBloc(sl()));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**App initialization flow**:
|
||
|
|
```dart
|
||
|
|
Future<void> main() async {
|
||
|
|
WidgetsFlutterBinding.ensureInitialized();
|
||
|
|
await configureDependencies();
|
||
|
|
|
||
|
|
final authBloc = sl<AuthBloc>();
|
||
|
|
authBloc.add(AuthStarted()); // Check local token
|
||
|
|
|
||
|
|
runApp(MyApp(authBloc: authBloc));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Complete flow**:
|
||
|
|
```
|
||
|
|
App Start
|
||
|
|
|
|
||
|
|
AuthStarted event
|
||
|
|
|
|
||
|
|
Check TokenStorage
|
||
|
|
|
|
||
|
|
Has token? --No--> AuthUnauthenticated -> Show login
|
||
|
|
|
|
||
|
|
Yes
|
||
|
|
|
|
||
|
|
Validate token (call /auth/refresh)
|
||
|
|
|
|
||
|
|
Success? --No--> Clear token -> AuthUnauthenticated
|
||
|
|
|
|
||
|
|
Yes
|
||
|
|
|
|
||
|
|
AuthAuthenticated -> Navigate to /home
|
||
|
|
```
|
||
|
|
|
||
|
|
## Decisions
|
||
|
|
|
||
|
|
1. **Adjust Flutter UI to match backend API** - Move password to Step1, call `/signup/start` with all data
|
||
|
|
2. **Keep invite code UI but don't send** - Backend doesn't support yet, preserve UI for future
|
||
|
|
3. **Remove OTP login entry** - Backend doesn't have OTP login API
|
||
|
|
4. **Use complete Bloc architecture** - AuthBloc + Cubits for forms, better testability and extensibility
|
||
|
|
|
||
|
|
## Risks
|
||
|
|
|
||
|
|
- Token refresh race conditions: ApiInterceptor should handle concurrent requests during refresh
|
||
|
|
- Secure storage availability: fallback to SharedPreferences on platforms without secure storage
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
- Unit tests for: Cubits, AuthBloc, Repository, API client
|
||
|
|
- Widget tests for: form validation, error display
|
||
|
|
- Integration tests for: complete signup/login flows
|