diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index f1e3ff3..58d1fba 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -39,7 +39,8 @@ func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permiss OrganizationBase: model.OrganizationBase{ PermissionBound: model.PermissionBound{ Base: storable.Base{ - ID: orgRef, + ID: orgRef, + CreatedAt: time.Now(), }, PermissionRef: permissionRef, OrganizationBoundBase: model.OrganizationBoundBase{ diff --git a/frontend/pweb/lib/app/router/router.dart b/frontend/pweb/lib/app/router/router.dart index ee95877..33e7670 100644 --- a/frontend/pweb/lib/app/router/router.dart +++ b/frontend/pweb/lib/app/router/router.dart @@ -1,8 +1,10 @@ import 'package:go_router/go_router.dart'; import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/app/router/page_params.dart'; import 'package:pweb/pages/2fa/page.dart'; import 'package:pweb/pages/signup/page.dart'; +import 'package:pweb/pages/verification/page.dart'; import 'package:pweb/widgets/sidebar/page.dart'; import 'package:pweb/pages/login/page.dart'; import 'package:pweb/pages/errors/not_found.dart'; @@ -29,28 +31,20 @@ GoRouter createRouter() => GoRouter( GoRoute( name: Pages.sfactor.name, path: routerPage(Pages.sfactor), - builder: (context, state) { - // Определяем откуда пришел пользователь - final isFromSignup = state.uri.queryParameters['from'] == 'signup'; - - return TwoFactorCodePage( - onVerificationSuccess: () { - if (isFromSignup) { - // После регистрации -> на страницу логина - context.goNamed(Pages.login.name); - } else { - // После логина -> на дашборд - context.goNamed(Pages.dashboard.name); - } - }, - ); - }, + builder: (context, _) => TwoFactorCodePage( + onVerificationSuccess: () => context.goNamed(Pages.dashboard.name), + ), ), GoRoute( name: Pages.signup.name, path: routerPage(Pages.signup), builder: (_, _) => const SignUpPage(), ), + GoRoute( + name: Pages.verify.name, + path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}', + builder: (_, state) => AccountVerificationPage(token: state.pathParameters[PageParams.token.name]!), + ), ], ), ], diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index b5d53d1..7b72074 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -433,5 +433,13 @@ "companyDescriptionHint": "Describe any of the fields of the Company's business", "optional": "optional", "ownerRole": "Organization Owner", - "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges" + "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges", + "accountVerificationFailed": "Oops! We failed to verify your account. Please, contact support", + "verifyAccount": "Account Verification", + "verificationFailed": "Verification Failed", + "verificationStatusUnknown": "We couldn't determine the status of your verification. Please try again later.", + "verificationStatusErrorUnknown": "Unexpected error occurred while verification. Try once again or contact support", + "accountVerified": "Account Verified!", + "accountVerifiedDescription": "Your account has been successfully verified. You can now log in to access your account.", + "retryVerification": "Retry Verification" } \ No newline at end of file diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 5303005..d2d4558 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -425,5 +425,13 @@ "noRecipientSelected": "Получатель не выбран", "ownerRole": "Владелец организации", - "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права" + "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права", + "accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.", + "verifyAccount": "Подтвердить аккаунт", + "verificationFailed": "Ошибка подтверждения", + "verificationStatusUnknown": "Не удалось определить статус подтверждения. Попробуйте позже", + "verificationStatusErrorUnknown": "Произошла непредвиденная ошибка при подтверждении. Попробуйте еще раз или обратитесь в службу поддержки", + "accountVerified": "Аккаунт подтвержден!", + "accountVerifiedDescription": "Ваш аккаунт успешно подтвержден. Теперь вы можете войти, чтобы получить доступ к своему аккаунту", + "retryVerification": "Повторить подтверждение" } \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 38f651d..ab4b73c 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -15,6 +15,7 @@ import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; import 'package:pweb/pages/signup/form/controllers.dart'; import 'package:pweb/pages/signup/form/form.dart'; +import 'package:pweb/utils/snackbar.dart'; import 'package:pweb/widgets/error/snackbar.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -82,10 +83,12 @@ class SignUpFormState extends State { void handleSignUp() => signUp( context, () { - context.goNamed( - Pages.sfactor.name, - queryParameters: {'from': 'signup'}, + final locs = AppLocalizations.of(context)!; + notifyUser( + context, + locs.signupSuccess(controllers.email.text.trim()), ); + context.goNamed(Pages.login.name); }, (e) => postNotifyUserOfErrorX( context: context, @@ -110,4 +113,4 @@ class SignUpFormState extends State { onSignUp: handleSignUp, onLogin: handleLogin, ); -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/status/failure.dart b/frontend/pweb/lib/pages/status/failure.dart new file mode 100644 index 0000000..25b9970 --- /dev/null +++ b/frontend/pweb/lib/pages/status/failure.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/errors/error.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class StatusPageFailure extends StatelessWidget { + final String errorMessage; + final Object error; + const StatusPageFailure({ super.key, required this.errorMessage, required this.error }); + + @override + Widget build(BuildContext context) => exceptionToErrorPage( + context: context, + title: AppLocalizations.of(context)!.verificationFailed, + errorMessage: errorMessage, + exception: error, + ); +} diff --git a/frontend/pweb/lib/pages/status/page.dart b/frontend/pweb/lib/pages/status/page.dart new file mode 100644 index 0000000..45a8564 --- /dev/null +++ b/frontend/pweb/lib/pages/status/page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/widgets/locale.dart'; + +import 'package:pweb/pages/status/failure.dart'; +import 'package:pweb/pages/status/success.dart'; +import 'package:pweb/pages/with_footer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class StatusPage extends StatefulWidget { + final Future Function() operation; + final String errorMessage; + final IconData? successIcon; + final String successMessage; + final String successDescription; + final Widget? successAction; + + const StatusPage({ + super.key, + required this.operation, + required this.errorMessage, + this.successIcon, + required this.successMessage, + required this.successDescription, + this.successAction, + }); + + @override + State> createState() => _StatusPageState(); +} + +class _StatusPageState extends State> { + late Future _operation; + + @override + void initState() { + super.initState(); + _operation = widget.operation(); + } + + @override + Widget build(BuildContext context) => PageWithFooter( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.verifyAccount), + centerTitle: true, + actions: [ + const LocaleChangerDropdown( + availableLocales: AppLocalizations.supportedLocales, + ), + ], + ), + child: FutureBuilder( + future: _operation, + builder: (context, snapshot) { + if (snapshot.hasError) { + return StatusPageFailure( + errorMessage: widget.errorMessage, + error: snapshot.error!, + ); + } + + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + return StatusPageSuccess( + successMessage: widget.successMessage, + successDescription: widget.successDescription, + icon: widget.successIcon, + action: widget.successAction, + ); + }, + ), + ); +} + diff --git a/frontend/pweb/lib/pages/status/success.dart b/frontend/pweb/lib/pages/status/success.dart new file mode 100644 index 0000000..acf3781 --- /dev/null +++ b/frontend/pweb/lib/pages/status/success.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + + +class StatusPageSuccess extends StatelessWidget { + final IconData? icon; + final String successMessage; + final String successDescription; + final Widget? action; + + const StatusPageSuccess({ + super.key, + required this.successMessage, + required this.successDescription, + this.action, + this.icon, + }); + + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon ?? Icons.check_circle_outline, size: 72, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 16.0), + Text( + successMessage, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8.0), + Text( + successDescription, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 20.0), + if (action != null) + action!, + ], + ), + ), + ); +} diff --git a/frontend/pweb/lib/pages/verification/page.dart b/frontend/pweb/lib/pages/verification/page.dart new file mode 100644 index 0000000..a8a35a7 --- /dev/null +++ b/frontend/pweb/lib/pages/verification/page.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/status/page.dart'; +import 'package:pweb/services/verification.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AccountVerificationPage extends StatelessWidget { + final String token; + + const AccountVerificationPage({super.key, required this.token}); + + @override + Widget build(BuildContext context) => StatusPage( + operation: () async => VerificationService.verify(token), + errorMessage: AppLocalizations.of(context)!.accountVerificationFailed, + successMessage: AppLocalizations.of(context)!.accountVerified, + successDescription: AppLocalizations.of(context)!.accountVerifiedDescription, + ); +} diff --git a/frontend/pweb/lib/services/verification.dart b/frontend/pweb/lib/services/verification.dart new file mode 100644 index 0000000..525975b --- /dev/null +++ b/frontend/pweb/lib/services/verification.dart @@ -0,0 +1,16 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/requests.dart'; + + + +class VerificationService { + static final _logger = Logger('VerificationService'); + + static Future verify(String token) async { + _logger.fine('Verifying token...'); + await getGETResponse(Services.account, '/verify/$token'); + return true; + } +}