feat(proj): init
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user