Files
please-pay-me/mobile/lib/features/auth/session_controller.dart
T
2026-09-21 04:06:43 +03:00

187 lines
5.5 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:please_pay_me/core/config/app_config.dart';
import 'package:please_pay_me/core/storage/session_storage.dart';
import 'package:please_pay_me/data/api/api_client.dart';
import 'package:please_pay_me/data/demo/demo_backend.dart';
import 'package:please_pay_me/data/models/auth_user.dart';
import 'package:please_pay_me/data/repositories/api_repositories.dart';
import 'package:please_pay_me/data/repositories/repositories.dart';
enum SessionStatus { restoring, signedOut, signedIn }
/// Owns authentication and hands out repositories bound to the current
/// session, so the rest of the app never sees tokens or base URLs.
class SessionController extends ChangeNotifier {
SessionController({
required AppConfig config,
required SessionStorage storage,
http.Client? httpClient,
DemoBackend? demoBackend,
}) : _config = config,
_storage = storage,
_httpClient = httpClient,
_demo = demoBackend ?? DemoBackend();
final SessionStorage _storage;
final http.Client? _httpClient;
final DemoBackend _demo;
AppConfig _config;
SessionStatus _status = SessionStatus.restoring;
AuthUser? _user;
String? _token;
String _baseUrl = '';
bool _isDemo = false;
String? _lastError;
SessionStatus get status => _status;
AuthUser? get user => _user;
String get baseUrl => _baseUrl.isEmpty ? _config.apiBaseUrl : _baseUrl;
String get webCabinetUrl => _config.webCabinetUrl;
/// API address baked in at build time; empty when it must be derived from
/// the cabinet URL the user typed.
String get configuredApiBaseUrl => _config.apiBaseUrl;
bool get isDemo => _isDemo;
String? get lastError => _lastError;
http.Client? get httpClient => _httpClient;
/// Changes whenever the backing data source changes, so feature controllers
/// can be rebuilt from scratch on login / logout.
String get sessionKey => '${_isDemo ? 'demo' : baseUrl}:${_user?.userId ?? 0}';
BudgetRepository get budgets => _repositories.budgets;
ExpenseRepository get expenses => _repositories.expenses;
JobRepository get jobs => _repositories.jobs;
UserRepository get users => _repositories.users;
_Repositories get _repositories {
if (_isDemo || _token == null) return _demoRepositories;
return _apiRepositories ??= _buildApiRepositories();
}
_Repositories? _apiRepositories;
late final _Repositories _demoRepositories = _Repositories(
budgets: _demo.budgets,
expenses: _demo.expenses,
jobs: _demo.jobs,
users: _demo.users,
);
_Repositories _buildApiRepositories() {
final client = ApiClient(
baseUrl: baseUrl,
tokenProvider: () => _token,
httpClient: _httpClient,
onUnauthorized: signOut,
);
return _Repositories(
budgets: ApiBudgetRepository(client),
expenses: ApiExpenseRepository(client),
jobs: ApiJobRepository(client),
users: ApiUserRepository(client),
);
}
Future<void> restore() async {
final stored = await _storage.readAll();
final token = stored['token'];
final baseUrl = stored['baseUrl'];
if (token != null && token.isNotEmpty && baseUrl != null && baseUrl.isNotEmpty) {
_token = token;
_baseUrl = baseUrl;
_isDemo = false;
_apiRepositories = null;
_user = AuthUser.tryDecode(stored['user']);
_status = SessionStatus.signedIn;
notifyListeners();
return;
}
if (_config.demoMode && _config.apiBaseUrl.isEmpty) {
_status = SessionStatus.signedOut;
notifyListeners();
return;
}
_status = SessionStatus.signedOut;
notifyListeners();
}
/// Signs in with a JWT issued by Telegram or Yandex (`POST /api/auth/*`).
Future<bool> signInWithToken({required String baseUrl, required String token}) async {
final normalizedUrl = baseUrl.trim().replaceAll(RegExp(r'/$'), '');
final normalizedToken = token.trim();
if (normalizedUrl.isEmpty || normalizedToken.isEmpty) {
_lastError = 'Укажите адрес кабинета и токен';
notifyListeners();
return false;
}
_lastError = null;
_baseUrl = normalizedUrl;
_token = normalizedToken;
_isDemo = false;
_apiRepositories = null;
try {
final user = await _repositories.users.me();
_user = user;
_status = SessionStatus.signedIn;
_config = _config.copyWith(apiBaseUrl: normalizedUrl, demoMode: false);
await _storage.write(
token: normalizedToken,
baseUrl: normalizedUrl,
user: user.encode(),
);
notifyListeners();
return true;
} on ApiException catch (error) {
_token = null;
_apiRepositories = null;
_lastError = error.message;
_status = SessionStatus.signedOut;
notifyListeners();
return false;
}
}
/// Runs the app against [DemoBackend] — no server required.
void startDemo() {
_isDemo = true;
_token = null;
_user = DemoBackend.user;
_lastError = null;
_status = SessionStatus.signedIn;
notifyListeners();
}
Future<void> signOut() async {
await _storage.clear();
_token = null;
_user = null;
_isDemo = false;
_apiRepositories = null;
_status = SessionStatus.signedOut;
notifyListeners();
}
}
class _Repositories {
const _Repositories({
required this.budgets,
required this.expenses,
required this.jobs,
required this.users,
});
final BudgetRepository budgets;
final ExpenseRepository expenses;
final JobRepository jobs;
final UserRepository users;
}