From 357af99564f79f3bea25f4a5bc7fd0e0e6b33bd1 Mon Sep 17 00:00:00 2001 From: Arseni Date: Wed, 26 Nov 2025 13:03:52 +0300 Subject: [PATCH] Added account permissions and ui for recipient --- frontend/pweb/devtools_options.yaml | 3 + frontend/pweb/lib/l10n/en.arb | 4 +- frontend/pweb/lib/l10n/ru.arb | 3 +- frontend/pweb/lib/main.dart | 19 +-- frontend/pweb/lib/pages/loaders/account.dart | 9 +- .../pweb/lib/pages/loaders/permissions.dart | 13 +- frontend/pweb/lib/pages/login/form.dart | 2 +- frontend/pweb/lib/pages/login/login.dart | 2 +- .../pweb/lib/pages/signup/form/state.dart | 4 +- frontend/pweb/lib/providers/account.dart | 97 +++++++++++++++ frontend/pweb/lib/providers/permissions.dart | 82 ++++++++++++ frontend/pweb/lib/providers/two_factor.dart | 38 ++---- frontend/pweb/lib/services/accounts.dart | 105 ++++++++++++++++ frontend/pweb/lib/services/mock_ids.dart | 12 ++ frontend/pweb/lib/services/permissions.dart | 117 ++++++++++++++++++ frontend/pweb/lib/utils/error_handler.dart | 3 + frontend/pweb/lib/widgets/drawer/avatar.dart | 2 +- .../pweb/lib/widgets/drawer/tiles/logout.dart | 2 +- frontend/pweb/lib/widgets/drawer/widget.dart | 2 +- .../pweb/lib/widgets/protected/widget.dart | 4 +- frontend/pweb/lib/widgets/sidebar/page.dart | 43 ++++++- .../pweb/lib/widgets/sidebar/sidebar.dart | 9 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + 23 files changed, 507 insertions(+), 70 deletions(-) create mode 100644 frontend/pweb/devtools_options.yaml create mode 100644 frontend/pweb/lib/providers/account.dart create mode 100644 frontend/pweb/lib/providers/permissions.dart create mode 100644 frontend/pweb/lib/services/accounts.dart create mode 100644 frontend/pweb/lib/services/mock_ids.dart create mode 100644 frontend/pweb/lib/services/permissions.dart diff --git a/frontend/pweb/devtools_options.yaml b/frontend/pweb/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/frontend/pweb/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 7a44b1b..f731400 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -36,9 +36,9 @@ "signupError": "Failed to signup: {error}", "signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.", "connectivityError": "Cannot reach the server at {serverAddress}. Check your network and try again.", - "errorAccountExists": "Account already exists", "errorAccountNotVerified": "Your account hasn't been verified yet. Please check your email to complete the verification", "errorLoginUnauthorized": "Login or password is incorrect. Please try again", + "errorAccountExists": "Account with this login already exists", "errorInternalError": "An internal error occurred. We're aware of the issue and working to resolve it. Please try again later", "errorVerificationTokenNotFound": "Account for verification not found. Sign up again", "created": "Created", @@ -465,4 +465,4 @@ "englishLanguage": "English", "russianLanguage": "Russian", "germanLanguage": "German" -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 79d873d..e1f9032 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -38,6 +38,7 @@ "connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.", "errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации", "errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова", + "errorAccountExists": "Аккаунт с таким логином уже существует", "errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже", "errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова", "created": "Создано", @@ -457,4 +458,4 @@ "englishLanguage": "Английский", "russianLanguage": "Русский", "germanLanguage": "Немецкий" -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 3354d40..e943a7e 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -8,7 +8,6 @@ import 'package:provider/provider.dart'; import 'package:logging/logging.dart'; import 'package:pshared/config/constants.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/organizations.dart'; @@ -16,7 +15,9 @@ import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; import 'package:pweb/providers/carousel.dart'; import 'package:pweb/providers/mock_payment.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/providers/operatioins.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/recipient.dart'; @@ -31,6 +32,8 @@ import 'package:pweb/services/payments/upload_history.dart'; import 'package:pweb/services/recipient/recipient.dart'; import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallets.dart'; +import 'package:pweb/services/accounts.dart'; +import 'package:pweb/services/permissions.dart'; void _setupLogging() { @@ -55,17 +58,15 @@ void main() async { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => LocaleProvider(null)), - ChangeNotifierProvider(create: (_) => AccountProvider()), - ChangeNotifierProxyProvider( - create: (context) => TwoFactorProvider( - accountProvider: context.read(), - ), - update: (context, accountProvider, previous) => TwoFactorProvider( - accountProvider: accountProvider, + ChangeNotifierProvider(create: (_) => PermissionsProvider(service: PermissionsService())), + ChangeNotifierProvider( + create: (context) => AccountProvider( + accountsService: AccountsService(), + permissionsProvider: context.read(), ), ), + ChangeNotifierProvider(create: (_) => TwoFactorProvider()), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), - ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), ChangeNotifierProvider( diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart index 8c78257..0e64ba1 100644 --- a/frontend/pweb/lib/pages/loaders/account.dart +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/error/snackbar.dart'; @@ -26,13 +26,10 @@ class AccountLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.account == null)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - provider.restore(); - }); + if (provider.account == null) { + WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login)); return const Center(child: CircularProgressIndicator()); } return child; }); } - diff --git a/frontend/pweb/lib/pages/loaders/permissions.dart b/frontend/pweb/lib/pages/loaders/permissions.dart index 1488048..1bf2098 100644 --- a/frontend/pweb/lib/pages/loaders/permissions.dart +++ b/frontend/pweb/lib/pages/loaders/permissions.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/account.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/error/snackbar.dart'; @@ -16,8 +17,10 @@ class PermissionsLoader extends StatelessWidget { const PermissionsLoader({super.key, required this.child}); @override - Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + Widget build(BuildContext context) => Consumer2(builder: (context, provider, accountProvider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } if (provider.error != null) { postNotifyUserOfErrorX( context: context, @@ -26,9 +29,9 @@ class PermissionsLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.permissions.isEmpty)) { + if (provider.error == null && !provider.hasLoaded && accountProvider.account != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - provider.load(); + provider.loadForAccount(accountProvider.account!.id); }); return const Center(child: CircularProgressIndicator()); } diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index bf230b1..ac5f506 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/login/buttons.dart'; diff --git a/frontend/pweb/lib/pages/login/login.dart b/frontend/pweb/lib/pages/login/login.dart index ba7c4e8..b1f804b 100644 --- a/frontend/pweb/lib/pages/login/login.dart +++ b/frontend/pweb/lib/pages/login/login.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/widgets/vspacer.dart'; diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 38f651d..5632a91 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -8,8 +8,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/models/describable.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; @@ -110,4 +110,4 @@ class SignUpFormState extends State { onSignUp: handleSignUp, onLogin: handleLogin, ); -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/providers/account.dart b/frontend/pweb/lib/providers/account.dart new file mode 100644 index 0000000..0aa352b --- /dev/null +++ b/frontend/pweb/lib/providers/account.dart @@ -0,0 +1,97 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/models/describable.dart'; + +import 'package:pweb/providers/permissions.dart'; +import 'package:pweb/services/accounts.dart'; + +class AccountProvider extends ChangeNotifier { + final AccountsService _accountsService; + final PermissionsProvider _permissionsProvider; + + AccountProvider({ + required AccountsService accountsService, + required PermissionsProvider permissionsProvider, + }) : _accountsService = accountsService, + _permissionsProvider = permissionsProvider; + + Account? _account; + bool _isLoading = false; + Object? _error; + + Account? get account => _account; + bool get isLoading => _isLoading; + Object? get error => _error; + bool get isLoggedIn => _account != null; + + PendingLogin? get pendingLogin => null; + + Future login({ + required String email, + required String password, + required String locale, + }) async { + _setLoading(true); + try { + final result = await _accountsService.login(email, password, locale: locale); + _account = result.account; + _error = null; + await _permissionsProvider.loadForAccount(result.account.id); + return LoginOutcome.completed(result.account); + } catch (e) { + _error = e; + rethrow; + } finally { + _setLoading(false); + } + } + + void completePendingLogin(Account account) { + _account = account; + _permissionsProvider.loadForAccount(account.id); + notifyListeners(); + } + + Future restore() async { + _setLoading(true); + _account = null; + _permissionsProvider.clear(); + _error = Exception('Сохраненная сессия отсутствует'); + _setLoading(false); + return null; + } + + Future signup({ + required AccountData account, + required Describable organization, + required String timezone, + required Describable ownerRole, + }) async { + _setLoading(true); + _error = null; + try { + await _accountsService.signup(account); + } catch (e) { + _error = e; + rethrow; + } finally { + _setLoading(false); + } + } + + Future logout() async { + _account = null; + _error = null; + _permissionsProvider.clear(); + notifyListeners(); + } + + void _setLoading(bool value) { + _isLoading = value; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/permissions.dart b/frontend/pweb/lib/providers/permissions.dart new file mode 100644 index 0000000..eb725c5 --- /dev/null +++ b/frontend/pweb/lib/providers/permissions.dart @@ -0,0 +1,82 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/permissions/action.dart' as perm; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/effect.dart'; +import 'package:pshared/models/resources.dart'; + +import 'package:pweb/services/permissions.dart'; +import 'package:pweb/services/mock_ids.dart'; + +class PermissionsProvider extends ChangeNotifier { + final PermissionsService _service; + + PermissionsProvider({required PermissionsService service}) : _service = service; + + bool _isLoading = false; + Object? _error; + String? _accountRef; + bool _hasLoaded = false; + String? _roleRef; + List _permissions = []; + List _policyDescriptions = []; + + bool get isLoading => _isLoading; + Object? get error => _error; + bool get isReady => _hasLoaded && !_isLoading && _error == null; + List get permissions => List.unmodifiable(_permissions); + bool get hasLoaded => _hasLoaded; + + bool get isCompany => _roleRef == companyRoleId; + bool get isRecipient => _roleRef == recipientRoleId; + + Future loadForAccount(String accountRef) async { + _accountRef = accountRef; + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final access = await _service.loadForAccount(accountRef); + _permissions = access.permissions.permissions; + _policyDescriptions = access.descriptions.policies; + _roleRef = access.permissions.roles.firstOrNull?.descriptionRef; + } catch (e) { + _permissions = []; + _policyDescriptions = []; + _error = e; + _roleRef = null; + } finally { + _hasLoaded = true; + _isLoading = false; + notifyListeners(); + } + } + + void clear() { + _accountRef = null; + _permissions = []; + _policyDescriptions = []; + _error = null; + _hasLoaded = false; + _roleRef = null; + notifyListeners(); + } + + bool canAccessResource(ResourceType resource, {perm.Action? action}) { + final policy = _policyDescriptions.firstWhereOrNull( + (policy) => (policy.resourceTypes?.contains(resource) ?? false), + ); + if (policy == null) return false; + + return _permissions.any( + (permission) => + permission.accountRef == _accountRef && + permission.policy.descriptionRef == policy.storable.id && + permission.policy.effect.effect == Effect.allow && + (action == null || permission.policy.effect.action == action), + ); + } +} diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index f23bb52..4a5cfdf 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -1,15 +1,13 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:pshared/models/auth/pending_login.dart'; -import 'package:pshared/provider/account.dart'; -import 'package:pshared/service/account.dart'; +import 'package:pweb/services/auth.dart'; class TwoFactorProvider extends ChangeNotifier { static final _logger = Logger('provider.two_factor'); - final AccountProvider _accountProvider; + final AuthenticationService _authService; - TwoFactorProvider({required AccountProvider accountProvider}) : _accountProvider = accountProvider; + TwoFactorProvider({AuthenticationService? authService}) : _authService = authService ?? AuthenticationService(); bool _isSubmitting = false; bool _hasError = false; @@ -20,7 +18,6 @@ class TwoFactorProvider extends ChangeNotifier { bool get hasError => _hasError; bool get verificationSuccess => _verificationSuccess; String? get errorMessage => _errorMessage; - PendingLogin? get pendingLogin => _accountProvider.pendingLogin; Future submitCode(String code) async { @@ -31,16 +28,8 @@ class TwoFactorProvider extends ChangeNotifier { notifyListeners(); try { - final pending = _accountProvider.pendingLogin; - if (pending == null) { - throw Exception('No pending login available'); - } - final account = await AccountService.confirmLoginCode( - pending: pending, - code: code, - ); - _accountProvider.completePendingLogin(account); - _verificationSuccess = true; + final isValid = await _authService.verifyTwoFactorCode(code); + _verificationSuccess = isValid; } catch (e) { _hasError = true; _errorMessage = e.toString(); @@ -52,18 +41,9 @@ class TwoFactorProvider extends ChangeNotifier { } Future resendCode() async { - final pending = _accountProvider.pendingLogin; - if (pending == null) { - _logger.warning('No pending login to resend code for'); - return; - } - try { - await AccountService.resendLoginCode(pending); - } catch (e) { - _logger.warning('Failed to resend login code', e); - _hasError = true; - _errorMessage = e.toString(); - notifyListeners(); - } + _logger.fine('Resending mock two-factor code'); + _hasError = false; + _errorMessage = null; + notifyListeners(); } } diff --git a/frontend/pweb/lib/services/accounts.dart b/frontend/pweb/lib/services/accounts.dart new file mode 100644 index 0000000..d840bbc --- /dev/null +++ b/frontend/pweb/lib/services/accounts.dart @@ -0,0 +1,105 @@ +import 'package:collection/collection.dart'; + +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/storable.dart'; + +import 'mock_ids.dart'; + +class InvalidCredentialsException implements Exception { + @override + String toString() => 'InvalidCredentialsException'; +} + +class DuplicateAccountException implements Exception { + @override + String toString() => 'DuplicateAccountException'; +} + +class AccountLoginResult { + final Account account; + final String roleId; + + const AccountLoginResult({ + required this.account, + required this.roleId, + }); +} + +class _AccountRecord { + final Account account; + final String password; + final String roleId; + + const _AccountRecord({ + required this.account, + required this.password, + required this.roleId, + }); +} + +class AccountsService { + final List<_AccountRecord> _accounts = [ + _AccountRecord( + account: Account( + storable: newStorable(id: companyAccountRef), + describable: newDescribable(name: 'Sendico Company'), + avatarUrl: null, + lastName: 'Owner', + login: 'company@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: companyRoleId, + ), + _AccountRecord( + account: Account( + storable: newStorable(id: recipientAccountRef), + describable: newDescribable(name: 'John Recipient'), + avatarUrl: null, + lastName: 'Doe', + login: 'recipient@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: recipientRoleId, + ), + ]; + + Future login(String email, String password, {String? locale}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = email.trim().toLowerCase(); + final record = _accounts.where((acc) => acc.account.login.toLowerCase() == normalized).singleOrNull; + + if (record == null || record.password != password) { + throw InvalidCredentialsException(); + } + + return AccountLoginResult(account: record.account, roleId: record.roleId); + } + + Future signup(AccountData data, {String roleId = recipientRoleId}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = data.login.trim().toLowerCase(); + if (_accounts.any((acc) => acc.account.login.toLowerCase() == normalized)) { + throw DuplicateAccountException(); + } + + final account = Account( + storable: newStorable(id: 'account-${_accounts.length + 1}'), + describable: newDescribable(name: data.name), + avatarUrl: null, + lastName: data.lastName, + login: normalized, + locale: data.locale, + ); + + _accounts.add(_AccountRecord(account: account, password: data.password, roleId: roleId)); + return AccountLoginResult(account: account, roleId: roleId); + } + + Account? getByRef(String accountRef) => _accounts.where((acc) => acc.account.id == accountRef).singleOrNull?.account; +} diff --git a/frontend/pweb/lib/services/mock_ids.dart b/frontend/pweb/lib/services/mock_ids.dart new file mode 100644 index 0000000..ffde7ff --- /dev/null +++ b/frontend/pweb/lib/services/mock_ids.dart @@ -0,0 +1,12 @@ +// Centralized identifiers for mock auth/permission data to keep the +// mock services in sync and make it easy to swap in a real API later. +const String mockOrganizationRef = 'org-sendico'; + +const String companyRoleId = 'role-company'; +const String recipientRoleId = 'role-recipient'; + +const String companyAccountRef = 'account-company'; +const String recipientAccountRef = 'account-recipient'; + +const String accountsPolicyDescriptionId = 'policy-accounts'; +const String rolesPolicyDescriptionId = 'policy-roles'; diff --git a/frontend/pweb/lib/services/permissions.dart b/frontend/pweb/lib/services/permissions.dart new file mode 100644 index 0000000..e7ab248 --- /dev/null +++ b/frontend/pweb/lib/services/permissions.dart @@ -0,0 +1,117 @@ +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/permissions/access.dart'; +import 'package:pshared/models/permissions/action.dart'; +import 'package:pshared/models/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/data/permissions.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; +import 'package:pshared/models/permissions/data/role.dart'; +import 'package:pshared/models/permissions/descriptions/permissions.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/descriptions/role.dart'; +import 'package:pshared/models/permissions/effect.dart'; +import 'package:pshared/models/resources.dart'; +import 'package:pshared/models/storable.dart'; + +import 'mock_ids.dart'; + +class PermissionsService { + static const String _objectType = 'permissions'; + + Future loadForAccount(String accountRef) async { + await Future.delayed(const Duration(milliseconds: 200)); + final baseAccess = _buildMockUserAccess(); + + final roles = [...baseAccess.permissions.roles]; + final permissions = [...baseAccess.permissions.permissions]; + final policies = [...baseAccess.permissions.policies]; + + final hasAccount = roles.any((r) => r.accountRef == accountRef); + if (!hasAccount) { + roles.add(Role(accountRef: accountRef, descriptionRef: recipientRoleId, organizationRef: mockOrganizationRef)); + } + + final relevantRoleRefs = roles + .where((r) => r.accountRef == accountRef) + .map((r) => r.descriptionRef) + .toSet(); + + final filteredPolicies = permissions + .where((p) => p.accountRef == accountRef && relevantRoleRefs.contains(p.policy.roleDescriptionRef)) + .toList(); + + return UserAccess( + descriptions: baseAccess.descriptions, + permissions: PermissionsData( + roles: roles.where((r) => r.accountRef == accountRef).toList(), + policies: policies.where((p) => relevantRoleRefs.contains(p.roleDescriptionRef)).toList(), + permissions: filteredPolicies, + ), + ); + } + + UserAccess _buildMockUserAccess() { + final roleDescriptions = [ + RoleDescription( + storable: newStorable(id: companyRoleId), + describable: newDescribable(name: 'Компания'), + organizationRef: mockOrganizationRef, + ), + RoleDescription( + storable: newStorable(id: recipientRoleId), + describable: newDescribable(name: 'Получатель'), + organizationRef: mockOrganizationRef, + ), + ]; + + final policyDescriptions = [ + PolicyDescription( + storable: newStorable(id: accountsPolicyDescriptionId), + describable: newDescribable(name: 'Управление аккаунтами'), + resourceTypes: const [ResourceType.accounts], + organizationRef: mockOrganizationRef, + ), + PolicyDescription( + storable: newStorable(id: rolesPolicyDescriptionId), + describable: newDescribable(name: 'Управление ролями'), + resourceTypes: const [ResourceType.roles], + organizationRef: mockOrganizationRef, + ), + ]; + + final companyAccountsPolicy = Policy( + roleDescriptionRef: companyRoleId, + organizationRef: mockOrganizationRef, + descriptionRef: accountsPolicyDescriptionId, + objectRef: null, + effect: const ActionEffect(action: Action.read, effect: Effect.allow), + ); + + final companyRolesPolicy = Policy( + roleDescriptionRef: companyRoleId, + organizationRef: mockOrganizationRef, + descriptionRef: rolesPolicyDescriptionId, + objectRef: null, + effect: const ActionEffect(action: Action.read, effect: Effect.allow), + ); + + final roles = [ + Role(accountRef: companyAccountRef, descriptionRef: companyRoleId, organizationRef: mockOrganizationRef), + Role(accountRef: recipientAccountRef, descriptionRef: recipientRoleId, organizationRef: mockOrganizationRef), + ]; + + final permissions = [ + Permission(policy: companyAccountsPolicy, accountRef: companyAccountRef), + Permission(policy: companyRolesPolicy, accountRef: companyAccountRef), + ]; + + return UserAccess( + descriptions: PermissionsDescription(roles: roleDescriptions, policies: policyDescriptions), + permissions: PermissionsData( + roles: roles, + policies: [companyAccountsPolicy, companyRolesPolicy], + permissions: permissions, + ), + ); + } +} diff --git a/frontend/pweb/lib/utils/error_handler.dart b/frontend/pweb/lib/utils/error_handler.dart index 2313b4f..90625da 100644 --- a/frontend/pweb/lib/utils/error_handler.dart +++ b/frontend/pweb/lib/utils/error_handler.dart @@ -5,6 +5,7 @@ import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/config/constants.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/services/accounts.dart'; class ErrorHandler { @@ -48,6 +49,8 @@ class ErrorHandler { final errorHandlers = { ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse), ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError), + InvalidCredentialsException: (_) => locs.errorLoginUnauthorized, + DuplicateAccountException: (_) => locs.errorAccountExists, }; return errorHandlers[e.runtimeType]?.call(e) ?? e.toString(); diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart index 632ea53..5d969f3 100644 --- a/frontend/pweb/lib/widgets/drawer/avatar.dart +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; diff --git a/frontend/pweb/lib/widgets/drawer/tiles/logout.dart b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart index fd4561a..a136d75 100644 --- a/frontend/pweb/lib/widgets/drawer/tiles/logout.dart +++ b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; diff --git a/frontend/pweb/lib/widgets/drawer/widget.dart b/frontend/pweb/lib/widgets/drawer/widget.dart index 5649bbc..4e157c0 100644 --- a/frontend/pweb/lib/widgets/drawer/widget.dart +++ b/frontend/pweb/lib/widgets/drawer/widget.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/resources.dart'; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/widgets/drawer/avatar.dart'; import 'package:pweb/widgets/drawer/tiles/dashboard.dart'; diff --git a/frontend/pweb/lib/widgets/protected/widget.dart b/frontend/pweb/lib/widgets/protected/widget.dart index 49ce816..5d85206 100644 --- a/frontend/pweb/lib/widgets/protected/widget.dart +++ b/frontend/pweb/lib/widgets/protected/widget.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/models/resources.dart'; import 'package:pshared/models/permissions/action.dart' as perm; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/permissions.dart'; T? protectedWidgetctx(BuildContext context, ResourceType resource, T child, {perm.Action? action}) { @@ -13,4 +13,4 @@ T? protectedWidgetctx(BuildContext context, ResourceType resou T? protectedWidget(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) { return provider.canAccessResource(resource, action: action) ? child : null; -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index bfb3c37..d5b0bc6 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -9,7 +9,10 @@ import 'package:pweb/pages/payout_page/page.dart'; import 'package:pweb/pages/payout_page/wallet/edit/page.dart'; import 'package:pweb/pages/report/page.dart'; import 'package:pweb/pages/settings/profile/page.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/permissions.dart'; +import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/appbar/app_bar.dart'; import 'package:pweb/pages/dashboard/dashboard.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; @@ -21,13 +24,37 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PageSelector extends StatelessWidget { const PageSelector({super.key}); + void _handleLogout(BuildContext context) { + context.read().logout(); + context.read().clear(); + navigateAndReplace(context, Pages.login); + } + @override Widget build(BuildContext context) { final provider = context.watch(); + final permissions = context.watch(); + final account = context.watch().account; final loc = AppLocalizations.of(context)!; + final allowedDestinations = permissions.isRecipient + ? { + PayoutDestination.settings, + PayoutDestination.methods, + PayoutDestination.editwallet, + } + : PayoutDestination.values.toSet(); + + final selected = allowedDestinations.contains(provider.selected) + ? provider.selected + : (permissions.isRecipient ? PayoutDestination.settings : PayoutDestination.dashboard); + + if (selected != provider.selected) { + WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(selected)); + } + Widget content; - switch (provider.selected) { + switch (selected) { case PayoutDestination.dashboard: content = DashboardPage( onRecipientSelected: (recipient) => @@ -83,14 +110,14 @@ class PageSelector extends StatelessWidget { break; default: - content = Text(provider.selected.name); + content = Text(selected.name); } return Scaffold( appBar: PayoutAppBar( - title: Text(provider.selected.localizedLabel(context)), + title: Text(selected.localizedLabel(context)), onAddFundsPressed: () {}, - onLogout: () => debugPrint('Logout clicked'), + onLogout: () => _handleLogout(context), ), body: Padding( padding: const EdgeInsets.only(left: 200, top: 40, right: 200), @@ -99,9 +126,13 @@ class PageSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PayoutSidebar( - selected: provider.selected, + selected: selected, onSelected: provider.selectPage, - onLogout: () => debugPrint('Logout clicked'), + onLogout: () => _handleLogout(context), + userName: account?.name, + items: permissions.isRecipient + ? const [PayoutDestination.settings, PayoutDestination.methods] + : null, ), Expanded(child: content), ], diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart index d3dac81..b51e012 100644 --- a/frontend/pweb/lib/widgets/sidebar/sidebar.dart +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -13,6 +13,7 @@ class PayoutSidebar extends StatelessWidget { this.onLogout, this.userName, this.avatarUrl, + this.items, }); final PayoutDestination selected; @@ -21,11 +22,13 @@ class PayoutSidebar extends StatelessWidget { final String? userName; final String? avatarUrl; + final List? items; @override Widget build(BuildContext context) { - final items = [ + final menuItems = items ?? + [ PayoutDestination.dashboard, PayoutDestination.recipients, PayoutDestination.methods, @@ -49,11 +52,11 @@ class PayoutSidebar extends StatelessWidget { theme: theme, avatarUrl: avatarUrl, userName: userName, - items: items, + items: menuItems, selected: selected, onSelected: onSelected, ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift index 33de092..79f5652 100644 --- a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import amplitude_flutter import file_selector_macos import flutter_timezone +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))