import 'package:dio/dio.dart'; import 'package:fintracker_api/fintracker_api.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../config.dart'; import 'token_store.dart'; enum AuthStatus { unknown, signedOut, signedIn } class AuthState { const AuthState(this.status, {this.accessToken, this.email}); final AuthStatus status; final String? accessToken; final String? email; bool get signedIn => status == AuthStatus.signedIn; } final tokenStoreProvider = Provider((_) => TokenStore()); /// A bare Dio for auth calls: no auth interceptor, so a refresh can never recurse. final authDioProvider = Provider( (_) => Dio(BaseOptions( baseUrl: apiBaseUrl(), connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 20), )), ); final authControllerProvider = NotifierProvider(AuthController.new); class AuthController extends Notifier { @override AuthState build() { Future.microtask(_restore); return const AuthState(AuthStatus.unknown); } AuthApi get _api => AuthApi(ref.read(authDioProvider)); TokenStore get _store => ref.read(tokenStoreProvider); Future _restore() async { final refresh = await _store.readRefresh(); if (refresh == null) { state = const AuthState(AuthStatus.signedOut); return; } final ok = await refreshSession(refresh); if (!ok) state = const AuthState(AuthStatus.signedOut); } Future login(String email, String password) async { try { final r = await _api.authLogin(loginRequest: LoginRequest(email: email, password: password)); await _accept(r.data!); return null; } on DioException catch (e) { return problemMessage(e); } } /// Exchange a refresh token for a new pair. Returns false when it is no longer valid. Future refreshSession([String? refresh]) async { final token = refresh ?? await _store.readRefresh(); if (token == null) return false; try { 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(); return false; } } Future _accept(TokenPair pair) async { await _store.writeRefresh(pair.refreshToken); state = AuthState(AuthStatus.signedIn, accessToken: pair.accessToken, email: state.email); } Future logout() async { final refresh = await _store.readRefresh(); if (refresh != null) { try { await _api.authLogout(refreshRequest: RefreshRequest(refreshToken: refresh)); } on DioException { // the server may already consider it revoked; local state wins } } await _store.clear(); state = const AuthState(AuthStatus.signedOut); } } /// Human-readable text from an RFC 7807 body, falling back to the transport error. String problemMessage(DioException e) { final data = e.response?.data; if (data is Map) { final detail = data['detail'] ?? data['title']; if (detail is String && detail.isNotEmpty) return detail; } return switch (e.type) { DioExceptionType.connectionError || DioExceptionType.connectionTimeout || DioExceptionType.receiveTimeout => 'Сервер недоступен', _ => e.message ?? 'Ошибка запроса', }; }