forked from baron/baron-sso
fix userfront verify link routing
This commit is contained in:
@@ -7,6 +7,7 @@ bool isPublicAuthPath(String path, Uri uri) {
|
||||
path == '/registration' ||
|
||||
path == '/verify' ||
|
||||
path == '/verification' ||
|
||||
path == '/verify-complete' ||
|
||||
path.startsWith('/verify/') ||
|
||||
path.startsWith('/l/') ||
|
||||
path == '/approve' ||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
|
||||
const verificationRoutePath = '/verify';
|
||||
const verificationCompletionRoutePath = '/verify-complete';
|
||||
const verificationCompletionRouteName = 'verify-complete';
|
||||
|
||||
String buildLocalizedVerificationCompletePath(String localeCode) {
|
||||
return '/$localeCode$verificationCompletionRoutePath';
|
||||
}
|
||||
|
||||
bool isDedicatedVerificationRoute(Uri uri) {
|
||||
final path = stripLocalePath(uri);
|
||||
return path == verificationRoutePath ||
|
||||
path == '/verification' ||
|
||||
path.startsWith('/verify/') ||
|
||||
path.startsWith('/l/');
|
||||
}
|
||||
|
||||
bool hasVerificationPayload(Uri uri) {
|
||||
final query = uri.queryParameters;
|
||||
final token = query['t'];
|
||||
final loginId = query['loginId'];
|
||||
final code = query['code'];
|
||||
return (token != null && token.isNotEmpty) ||
|
||||
(loginId != null &&
|
||||
loginId.isNotEmpty &&
|
||||
code != null &&
|
||||
code.isNotEmpty);
|
||||
}
|
||||
|
||||
String? buildDedicatedVerificationRedirect(
|
||||
Uri uri, {
|
||||
required String localeCode,
|
||||
}) {
|
||||
if (isDedicatedVerificationRoute(uri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final query = uri.queryParameters;
|
||||
final token = query['t'];
|
||||
final loginId = query['loginId'];
|
||||
final code = query['code'];
|
||||
final pendingRef = query['pendingRef'];
|
||||
final sanitizedQuery = <String, String>{};
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
sanitizedQuery['t'] = token;
|
||||
} else if (loginId != null &&
|
||||
loginId.isNotEmpty &&
|
||||
code != null &&
|
||||
code.isNotEmpty) {
|
||||
sanitizedQuery['loginId'] = loginId;
|
||||
sanitizedQuery['code'] = code;
|
||||
if (pendingRef != null && pendingRef.isNotEmpty) {
|
||||
sanitizedQuery['pendingRef'] = pendingRef;
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitizedQuery.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Uri(
|
||||
path: '/$localeCode$verificationRoutePath',
|
||||
queryParameters: sanitizedQuery,
|
||||
).toString();
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import '../../../core/notifiers/auth_notifier.dart';
|
||||
import '../domain/login_challenge_resolver.dart';
|
||||
import '../domain/cookie_session_policy.dart';
|
||||
import '../domain/login_link_route_policy.dart';
|
||||
import '../domain/verification_completion_route.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../../../core/services/web_window.dart';
|
||||
import '../../../core/ui/toast_service.dart';
|
||||
@@ -26,12 +27,14 @@ class LoginScreen extends ConsumerStatefulWidget {
|
||||
final String? verificationToken;
|
||||
final String? loginChallenge;
|
||||
final String? redirectUrl;
|
||||
final bool verificationCompleteOnly;
|
||||
|
||||
const LoginScreen({
|
||||
super.key,
|
||||
this.verificationToken,
|
||||
this.loginChallenge,
|
||||
this.redirectUrl,
|
||||
this.verificationCompleteOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -88,6 +91,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool _noticeHandled = false;
|
||||
bool _drySendEnabled = false;
|
||||
bool _oidcAutoAcceptTried = false;
|
||||
bool _verificationHandoffStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -100,7 +104,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_redirectUrl = widget.redirectUrl;
|
||||
_passwordFocusNode.addListener(_handlePasswordFocusChange);
|
||||
HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent);
|
||||
_verificationOnly = _isVerificationOnlyUri(Uri.base);
|
||||
_verificationOnly =
|
||||
widget.verificationCompleteOnly || _isVerificationOnlyUri(Uri.base);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final uri = Uri.base;
|
||||
@@ -124,12 +129,43 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final pendingRefParam = uri.queryParameters['pendingRef'];
|
||||
final shortCodeFromPath = extractLoginShortCode(uri);
|
||||
final hasShortCodePath = shortCodeFromPath != null;
|
||||
final hasTokenParam = uri.queryParameters.containsKey('t');
|
||||
final acceptsVerificationPayload = isDedicatedVerificationRoute(uri);
|
||||
final waitsForVerificationHandoff =
|
||||
!acceptsVerificationPayload && hasVerificationPayload(uri);
|
||||
final hasTokenParam =
|
||||
acceptsVerificationPayload && uri.queryParameters.containsKey('t');
|
||||
final hasVerificationToken =
|
||||
widget.verificationToken != null || hasTokenParam;
|
||||
final hasLoginCode = loginIdParam != null && codeParam != null;
|
||||
final hasLoginCode =
|
||||
acceptsVerificationPayload &&
|
||||
loginIdParam != null &&
|
||||
codeParam != null;
|
||||
final notice = uri.queryParameters['notice'];
|
||||
|
||||
if (widget.verificationCompleteOnly) {
|
||||
_markVerificationApproved(
|
||||
tr('msg.userfront.login.verification.approved_remote'),
|
||||
title: tr('ui.userfront.login.verification.title_remote'),
|
||||
actionLabel: tr('ui.userfront.login.verification.action_label_close'),
|
||||
onAction: _closeVerificationWindowIfPossible,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitsForVerificationHandoff) {
|
||||
final localeCode =
|
||||
extractLocaleFromPath(uri) ?? resolvePreferredLocaleCode();
|
||||
final target = buildDedicatedVerificationRedirect(
|
||||
uri,
|
||||
localeCode: localeCode,
|
||||
);
|
||||
if (target != null && mounted) {
|
||||
context.go(target);
|
||||
_startVerificationHandoff(Uri.parse(target));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasShortCodePath) {
|
||||
_verifyShortCode(shortCodeFromPath);
|
||||
}
|
||||
@@ -174,6 +210,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
|
||||
bool _isVerificationOnlyUri(Uri uri) {
|
||||
if (!isDedicatedVerificationRoute(uri) &&
|
||||
widget.verificationToken == null) {
|
||||
return false;
|
||||
}
|
||||
final loginIdParam = uri.queryParameters['loginId'];
|
||||
final codeParam = uri.queryParameters['code'];
|
||||
return widget.verificationToken != null ||
|
||||
@@ -182,6 +222,33 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
extractLoginShortCode(uri) != null;
|
||||
}
|
||||
|
||||
void _startVerificationHandoff(Uri targetUri) {
|
||||
if (_verificationHandoffStarted) {
|
||||
return;
|
||||
}
|
||||
_verificationHandoffStarted = true;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_verificationOnly = true;
|
||||
});
|
||||
} else {
|
||||
_verificationOnly = true;
|
||||
}
|
||||
|
||||
final loginIdParam = targetUri.queryParameters['loginId'];
|
||||
final codeParam = targetUri.queryParameters['code'];
|
||||
final pendingRefParam = targetUri.queryParameters['pendingRef'];
|
||||
final tokenParam = targetUri.queryParameters['t'];
|
||||
|
||||
if (loginIdParam != null && codeParam != null) {
|
||||
_verifyLoginCode(loginIdParam, codeParam, pendingRef: pendingRefParam);
|
||||
return;
|
||||
}
|
||||
if (tokenParam != null && tokenParam.isNotEmpty) {
|
||||
_verifyToken(tokenParam);
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePasswordFocusChange() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -730,6 +797,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _moveVerificationOnlyResultToCleanRoute() {
|
||||
if (!_verificationOnly || widget.verificationCompleteOnly) {
|
||||
return false;
|
||||
}
|
||||
final localeCode =
|
||||
extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode();
|
||||
context.go(buildLocalizedVerificationCompletePath(localeCode));
|
||||
return true;
|
||||
}
|
||||
|
||||
void _markVerificationApproved(
|
||||
String message, {
|
||||
String? title,
|
||||
@@ -746,6 +823,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
pageTitle ?? tr('ui.userfront.login.verification.page_title');
|
||||
final resolvedActionLabel =
|
||||
actionLabel ?? tr('ui.userfront.login.verification.action_label');
|
||||
if (_moveVerificationOnlyResultToCleanRoute()) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
|
||||
Reference in New Issue
Block a user