add refresh token support
This commit is contained in:
@@ -6,18 +6,20 @@ import 'package:mileograph_flutter/services/token_storage_service.dart';
|
||||
class AuthService extends ChangeNotifier {
|
||||
final ApiService api;
|
||||
bool _restoring = false;
|
||||
String? _accessToken;
|
||||
Future<bool>? _refreshFuture;
|
||||
|
||||
final TokenStorageService _tokenStorage = TokenStorageService();
|
||||
|
||||
AuthService({required this.api}) {
|
||||
api.setTokenProvider(() => token);
|
||||
api.setUnauthorizedHandler(handleTokenExpired);
|
||||
api.setUnauthorizedHandler(_handleUnauthorized);
|
||||
}
|
||||
|
||||
AuthenticatedUserData? _user;
|
||||
|
||||
bool get isLoggedIn => _user != null;
|
||||
String? get token => _user?.accessToken;
|
||||
String? get token => _accessToken;
|
||||
String? get userId => _user?.userId;
|
||||
String? get username => _user?.username;
|
||||
String? get fullName => _user?.fullName;
|
||||
@@ -33,11 +35,13 @@ class AuthService extends ChangeNotifier {
|
||||
required String fullName,
|
||||
required String accessToken,
|
||||
required String email,
|
||||
String? refreshToken,
|
||||
String entriesVisibility = 'private',
|
||||
String mileageVisibility = 'private',
|
||||
bool isElevated = false,
|
||||
bool isDisabled = false,
|
||||
}) {
|
||||
_accessToken = accessToken;
|
||||
_user = AuthenticatedUserData(
|
||||
userId: userId,
|
||||
username: username,
|
||||
@@ -49,7 +53,7 @@ class AuthService extends ChangeNotifier {
|
||||
isElevated: isElevated,
|
||||
disabled: isDisabled,
|
||||
);
|
||||
_persistToken(accessToken);
|
||||
_persistTokens(accessToken, refreshToken);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -64,8 +68,9 @@ class AuthService extends ChangeNotifier {
|
||||
};
|
||||
|
||||
// 1. Get token
|
||||
final tokenResponse = await api.postForm('/token', formData);
|
||||
final tokenResponse = await api.postForm('/token', formData, includeAuth: false);
|
||||
final accessToken = tokenResponse['access_token'];
|
||||
final refreshToken = tokenResponse['refresh_token'];
|
||||
|
||||
// 2. Get user details
|
||||
final userResponse = await api.get(
|
||||
@@ -83,6 +88,7 @@ class AuthService extends ChangeNotifier {
|
||||
fullName: userResponse['full_name'],
|
||||
accessToken: accessToken,
|
||||
email: userResponse['email'],
|
||||
refreshToken: refreshToken,
|
||||
entriesVisibility: _parseVisibility(
|
||||
userResponse['user_entries_visibility'] ?? userResponse['entries_visibility'],
|
||||
'private',
|
||||
@@ -103,33 +109,31 @@ class AuthService extends ChangeNotifier {
|
||||
// read token from secure storage (with fallback)
|
||||
final token = await _tokenStorage.getToken();
|
||||
if (token == null || token.isEmpty) return;
|
||||
_accessToken = token;
|
||||
|
||||
final userResponse = await api.get(
|
||||
'/users/me',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
setLoginData(
|
||||
userId: userResponse['user_id'],
|
||||
username: userResponse['username'],
|
||||
fullName: userResponse['full_name'],
|
||||
accessToken: token,
|
||||
email: userResponse['email'],
|
||||
entriesVisibility: _parseVisibility(
|
||||
userResponse['user_entries_visibility'] ?? userResponse['entries_visibility'],
|
||||
'private',
|
||||
),
|
||||
mileageVisibility: _parseVisibility(
|
||||
userResponse['user_mileage_visibility'] ?? userResponse['mileage_visibility'],
|
||||
'private',
|
||||
),
|
||||
isElevated: _parseIsElevated(userResponse),
|
||||
isDisabled: _parseIsDisabled(userResponse),
|
||||
);
|
||||
} catch (_) {
|
||||
final restoredAccessToken = _accessToken ?? token;
|
||||
setLoginData(
|
||||
userId: userResponse['user_id'],
|
||||
username: userResponse['username'],
|
||||
fullName: userResponse['full_name'],
|
||||
accessToken: restoredAccessToken,
|
||||
email: userResponse['email'],
|
||||
entriesVisibility: _parseVisibility(
|
||||
userResponse['user_entries_visibility'] ?? userResponse['entries_visibility'],
|
||||
'private',
|
||||
),
|
||||
mileageVisibility: _parseVisibility(
|
||||
userResponse['user_mileage_visibility'] ?? userResponse['mileage_visibility'],
|
||||
'private',
|
||||
),
|
||||
isElevated: _parseIsElevated(userResponse),
|
||||
isDisabled: _parseIsDisabled(userResponse),
|
||||
);
|
||||
} catch (_) {
|
||||
await _clearToken();
|
||||
} finally {
|
||||
_restoring = false;
|
||||
@@ -140,12 +144,9 @@ class AuthService extends ChangeNotifier {
|
||||
final token = await _tokenStorage.getToken();
|
||||
if (token == null || token.isEmpty) return false;
|
||||
try {
|
||||
_accessToken = token;
|
||||
await api.get(
|
||||
'/validate',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
@@ -154,11 +155,15 @@ class AuthService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistToken(String token) async {
|
||||
await _tokenStorage.setToken(token);
|
||||
Future<void> _persistTokens(String accessToken, String? refreshToken) async {
|
||||
await _tokenStorage.setToken(accessToken);
|
||||
if (refreshToken != null && refreshToken.isNotEmpty) {
|
||||
await _tokenStorage.setRefreshToken(refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearToken() async {
|
||||
_accessToken = null;
|
||||
await _tokenStorage.clearToken();
|
||||
}
|
||||
|
||||
@@ -181,6 +186,61 @@ class AuthService extends ChangeNotifier {
|
||||
await api.postForm('/register', formData);
|
||||
}
|
||||
|
||||
Future<bool> _handleUnauthorized() async {
|
||||
if (_refreshFuture != null) {
|
||||
return _refreshFuture!;
|
||||
}
|
||||
_refreshFuture = _refreshTokens();
|
||||
final refreshed = await _refreshFuture!;
|
||||
_refreshFuture = null;
|
||||
return refreshed;
|
||||
}
|
||||
|
||||
Future<bool> _refreshTokens() async {
|
||||
final refreshToken = await _tokenStorage.getRefreshToken();
|
||||
if (refreshToken == null || refreshToken.isEmpty) {
|
||||
await handleTokenExpired();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final response = await api.post(
|
||||
'/token/refresh',
|
||||
{'refresh_token': refreshToken},
|
||||
includeAuth: false,
|
||||
allowRetry: false,
|
||||
);
|
||||
final accessToken = response['access_token'];
|
||||
final newRefreshToken = response['refresh_token'];
|
||||
if (accessToken is! String ||
|
||||
accessToken.isEmpty ||
|
||||
newRefreshToken is! String ||
|
||||
newRefreshToken.isEmpty) {
|
||||
await handleTokenExpired();
|
||||
return false;
|
||||
}
|
||||
_accessToken = accessToken;
|
||||
await _persistTokens(accessToken, newRefreshToken);
|
||||
if (_user != null) {
|
||||
_user = AuthenticatedUserData(
|
||||
userId: _user!.userId,
|
||||
username: _user!.username,
|
||||
fullName: _user!.fullName,
|
||||
accessToken: accessToken,
|
||||
email: _user!.email,
|
||||
entriesVisibility: _user!.entriesVisibility,
|
||||
mileageVisibility: _user!.mileageVisibility,
|
||||
isElevated: _user!.isElevated,
|
||||
isDisabled: _user!.disabled,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (_) {
|
||||
await handleTokenExpired();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleTokenExpired() async {
|
||||
_user = null;
|
||||
await _clearToken();
|
||||
|
||||
Reference in New Issue
Block a user