+ token verification
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version unknown status

This commit is contained in:
Stephan D
2025-11-24 20:53:42 +01:00
parent b4f6f63871
commit d65e442cb6
10 changed files with 217 additions and 23 deletions

View File

@@ -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]!),
),
],
),
],

View File

@@ -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 organizations creator, providing full administrative privileges"
"ownerRoleDescription": "This role is granted to the organizations 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"
}

View File

@@ -425,5 +425,13 @@
"noRecipientSelected": "Получатель не выбран",
"ownerRole": "Владелец организации",
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права"
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права",
"accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.",
"verifyAccount": "Подтвердить аккаунт",
"verificationFailed": "Ошибка подтверждения",
"verificationStatusUnknown": "Не удалось определить статус подтверждения. Попробуйте позже",
"verificationStatusErrorUnknown": "Произошла непредвиденная ошибка при подтверждении. Попробуйте еще раз или обратитесь в службу поддержки",
"accountVerified": "Аккаунт подтвержден!",
"accountVerifiedDescription": "Ваш аккаунт успешно подтвержден. Теперь вы можете войти, чтобы получить доступ к своему аккаунту",
"retryVerification": "Повторить подтверждение"
}

View File

@@ -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<SignUpForm> {
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<SignUpForm> {
onSignUp: handleSignUp,
onLogin: handleLogin,
);
}
}

View File

@@ -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,
);
}

View File

@@ -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<T> extends StatefulWidget {
final Future<T> 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<StatusPage<T>> createState() => _StatusPageState<T>();
}
class _StatusPageState<T> extends State<StatusPage<T>> {
late Future<T> _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<T>(
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,
);
},
),
);
}

View File

@@ -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!,
],
),
),
);
}

View File

@@ -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<bool>(
operation: () async => VerificationService.verify(token),
errorMessage: AppLocalizations.of(context)!.accountVerificationFailed,
successMessage: AppLocalizations.of(context)!.accountVerified,
successDescription: AppLocalizations.of(context)!.accountVerifiedDescription,
);
}

View File

@@ -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<bool> verify(String token) async {
_logger.fine('Verifying token...');
await getGETResponse(Services.account, '/verify/$token');
return true;
}
}