Clean Architecture structure: - core/config - Environment configuration - core/theme - Material 3 theming with FlexColorScheme - core/error - Typed exception hierarchy - core/api - Dio HTTP clients with interceptors - core/auth - Authentication state management - routing - go_router with shell navigation - shared/layouts - Adaptive scaffold Dependencies added: - flutter_riverpod, riverpod_annotation, riverpod_generator - freezed, freezed_annotation, json_serializable - dio, go_router, shared_preferences - flex_color_scheme, flutter_adaptive_scaffold Features: - Responsive navigation (rail on desktop, bottom on mobile) - Dashboard placeholder with welcome card - Theme switching infrastructure - API client ready for Core API and Tatlock API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
128 lines
4.0 KiB
Dart
128 lines
4.0 KiB
Dart
import 'dart:developer' as developer;
|
|
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'auth_state.dart';
|
|
|
|
part 'auth_provider.g.dart';
|
|
|
|
/// Provides authentication state and operations.
|
|
///
|
|
/// Note: Full OIDC implementation with flutter_appauth requires
|
|
/// native platform configuration. For now, this provides the
|
|
/// state management infrastructure.
|
|
@riverpod
|
|
class AuthNotifier extends _$AuthNotifier {
|
|
static const _accessTokenKey = 'auth_access_token';
|
|
static const _refreshTokenKey = 'auth_refresh_token';
|
|
static const _expiresAtKey = 'auth_expires_at';
|
|
static const _userIdKey = 'auth_user_id';
|
|
static const _userNameKey = 'auth_user_name';
|
|
static const _userEmailKey = 'auth_user_email';
|
|
|
|
@override
|
|
Future<AuthState> build() async {
|
|
return _loadStoredAuth();
|
|
}
|
|
|
|
Future<AuthState> _loadStoredAuth() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
final accessToken = prefs.getString(_accessTokenKey);
|
|
if (accessToken == null) {
|
|
return const AuthState();
|
|
}
|
|
|
|
final expiresAtMs = prefs.getInt(_expiresAtKey);
|
|
final expiresAt = expiresAtMs != null
|
|
? DateTime.fromMillisecondsSinceEpoch(expiresAtMs)
|
|
: null;
|
|
|
|
final authState = AuthState(
|
|
isAuthenticated: true,
|
|
accessToken: accessToken,
|
|
refreshToken: prefs.getString(_refreshTokenKey),
|
|
expiresAt: expiresAt,
|
|
userId: prefs.getString(_userIdKey),
|
|
userName: prefs.getString(_userNameKey),
|
|
userEmail: prefs.getString(_userEmailKey),
|
|
);
|
|
|
|
// Check if token is expired
|
|
if (authState.isTokenExpired) {
|
|
developer.log('Stored token expired, clearing auth', name: 'auth');
|
|
await _clearStoredAuth();
|
|
return const AuthState();
|
|
}
|
|
|
|
developer.log('Restored auth for ${authState.userName}', name: 'auth');
|
|
return authState;
|
|
} catch (e) {
|
|
developer.log('Failed to load stored auth: $e', name: 'auth');
|
|
return const AuthState();
|
|
}
|
|
}
|
|
|
|
/// Sign in with OIDC (placeholder for flutter_appauth integration).
|
|
Future<void> signIn() async {
|
|
// TODO: Implement OIDC flow with flutter_appauth
|
|
// For now, this is a placeholder that will be implemented
|
|
// when native platform configuration is complete.
|
|
developer.log('Sign in requested - OIDC not yet configured', name: 'auth');
|
|
}
|
|
|
|
/// Sign out and clear stored credentials.
|
|
Future<void> signOut() async {
|
|
await _clearStoredAuth();
|
|
state = const AsyncData(AuthState());
|
|
developer.log('Signed out', name: 'auth');
|
|
}
|
|
|
|
/// Update auth state (called after successful OIDC flow).
|
|
Future<void> setAuthenticated({
|
|
required String accessToken,
|
|
String? refreshToken,
|
|
DateTime? expiresAt,
|
|
String? userId,
|
|
String? userName,
|
|
String? userEmail,
|
|
}) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setString(_accessTokenKey, accessToken);
|
|
if (refreshToken != null) {
|
|
await prefs.setString(_refreshTokenKey, refreshToken);
|
|
}
|
|
if (expiresAt != null) {
|
|
await prefs.setInt(_expiresAtKey, expiresAt.millisecondsSinceEpoch);
|
|
}
|
|
if (userId != null) await prefs.setString(_userIdKey, userId);
|
|
if (userName != null) await prefs.setString(_userNameKey, userName);
|
|
if (userEmail != null) await prefs.setString(_userEmailKey, userEmail);
|
|
|
|
state = AsyncData(AuthState(
|
|
isAuthenticated: true,
|
|
accessToken: accessToken,
|
|
refreshToken: refreshToken,
|
|
expiresAt: expiresAt,
|
|
userId: userId,
|
|
userName: userName,
|
|
userEmail: userEmail,
|
|
));
|
|
|
|
developer.log('Authenticated as $userName', name: 'auth');
|
|
}
|
|
|
|
Future<void> _clearStoredAuth() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_accessTokenKey);
|
|
await prefs.remove(_refreshTokenKey);
|
|
await prefs.remove(_expiresAtKey);
|
|
await prefs.remove(_userIdKey);
|
|
await prefs.remove(_userNameKey);
|
|
await prefs.remove(_userEmailKey);
|
|
}
|
|
}
|