feat(app): смена сервера API и очистка данных устройства при выходе
Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:fintracker_api/fintracker_api.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../cache/response_cache_provider.dart';
|
||||
import '../config.dart';
|
||||
import 'token_store.dart';
|
||||
|
||||
@@ -20,14 +21,18 @@ final tokenStoreProvider = Provider<TokenStore>((_) => TokenStore());
|
||||
|
||||
/// A bare Dio for auth calls: no auth interceptor, so a refresh can never recurse.
|
||||
final authDioProvider = Provider<Dio>(
|
||||
(_) => Dio(BaseOptions(
|
||||
baseUrl: apiBaseUrl(),
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
)),
|
||||
(ref) => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ref.watch(apiBaseUrlProvider),
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final authControllerProvider = NotifierProvider<AuthController, AuthState>(AuthController.new);
|
||||
final authControllerProvider = NotifierProvider<AuthController, AuthState>(
|
||||
AuthController.new,
|
||||
);
|
||||
|
||||
class AuthController extends Notifier<AuthState> {
|
||||
@override
|
||||
@@ -51,7 +56,9 @@ class AuthController extends Notifier<AuthState> {
|
||||
|
||||
Future<String?> login(String email, String password) async {
|
||||
try {
|
||||
final r = await _api.authLogin(loginRequest: LoginRequest(email: email, password: password));
|
||||
final r = await _api.authLogin(
|
||||
loginRequest: LoginRequest(email: email, password: password),
|
||||
);
|
||||
await _accept(r.data!);
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
@@ -64,32 +71,51 @@ class AuthController extends Notifier<AuthState> {
|
||||
final token = refresh ?? await _store.readRefresh();
|
||||
if (token == null) return false;
|
||||
try {
|
||||
final r = await _api.authRefresh(refreshRequest: RefreshRequest(refreshToken: token));
|
||||
final r = await _api.authRefresh(
|
||||
refreshRequest: RefreshRequest(refreshToken: token),
|
||||
);
|
||||
await _accept(r.data!);
|
||||
return true;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 401) await _store.clear();
|
||||
if (e.response?.statusCode == 401) await _wipeLocal();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _accept(TokenPair pair) async {
|
||||
await _store.writeRefresh(pair.refreshToken);
|
||||
state = AuthState(AuthStatus.signedIn, accessToken: pair.accessToken, email: state.email);
|
||||
state = AuthState(
|
||||
AuthStatus.signedIn,
|
||||
accessToken: pair.accessToken,
|
||||
email: state.email,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final refresh = await _store.readRefresh();
|
||||
if (refresh != null) {
|
||||
try {
|
||||
await _api.authLogout(refreshRequest: RefreshRequest(refreshToken: refresh));
|
||||
await _api.authLogout(
|
||||
refreshRequest: RefreshRequest(refreshToken: refresh),
|
||||
);
|
||||
} on DioException {
|
||||
// the server may already consider it revoked; local state wins
|
||||
}
|
||||
}
|
||||
await _store.clear();
|
||||
await _wipeLocal();
|
||||
state = const AuthState(AuthStatus.signedOut);
|
||||
}
|
||||
|
||||
/// Everything this device keeps for the account: the refresh token and the cached
|
||||
/// responses (the whole financial picture, in plain sqlite / IndexedDB).
|
||||
Future<void> _wipeLocal() async {
|
||||
await _store.clear();
|
||||
try {
|
||||
await ref.read(responseCacheDbProvider).clear();
|
||||
} on Object {
|
||||
// the token is already gone; a cache that cannot be opened must not block signing out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable text from an RFC 7807 body, falling back to the transport error.
|
||||
@@ -102,8 +128,7 @@ String problemMessage(DioException e) {
|
||||
return switch (e.type) {
|
||||
DioExceptionType.connectionError ||
|
||||
DioExceptionType.connectionTimeout ||
|
||||
DioExceptionType.receiveTimeout =>
|
||||
'Сервер недоступен',
|
||||
DioExceptionType.receiveTimeout => 'Сервер недоступен',
|
||||
_ => e.message ?? 'Ошибка запроса',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../config.dart';
|
||||
import 'auth_controller.dart';
|
||||
|
||||
/// Points the app at [url] (null = the default). A different server means a different
|
||||
/// account, so the session ends first — while the clients still talk to the OLD server, so
|
||||
/// the refresh token is revoked there and never sent to the new one — and only then is the
|
||||
/// address changed. Takes a container rather than a `WidgetRef`: signing out replaces the
|
||||
/// calling screen, and a disposed widget's `ref` throws.
|
||||
Future<void> switchApiBaseUrl(ProviderContainer container, String? url) async {
|
||||
final next = url ?? defaultApiBaseUrl();
|
||||
if (next != container.read(apiBaseUrlProvider)) {
|
||||
await container.read(authControllerProvider.notifier).logout();
|
||||
}
|
||||
await container.read(apiBaseUrlProvider.notifier).save(url);
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
/// (single-user, TLS-only deployment; see plan §7 open question 1).
|
||||
class TokenStore {
|
||||
TokenStore([FlutterSecureStorage? storage])
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
static const _refreshKey = 'refresh_token';
|
||||
|
||||
Future<String?> readRefresh() => _storage.read(key: _refreshKey);
|
||||
Future<void> writeRefresh(String token) => _storage.write(key: _refreshKey, value: token);
|
||||
Future<void> writeRefresh(String token) =>
|
||||
_storage.write(key: _refreshKey, value: token);
|
||||
Future<void> clear() => _storage.delete(key: _refreshKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user