forked from baron/baron-sso
리다이렉트 후속 로직 업데이트
This commit is contained in:
@@ -3,13 +3,13 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../../../core/widgets/language_selector.dart';
|
||||
import '../../../core/services/web_auth_integration.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/auth_token_store.dart';
|
||||
import '../../../core/services/oidc_redirect_guard.dart';
|
||||
import '../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../../../core/services/web_window.dart';
|
||||
@@ -65,9 +65,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool _verificationApproved = false;
|
||||
bool _dismissedOverlays = false;
|
||||
String _verificationMessage = '';
|
||||
String _verificationTitle = tr(
|
||||
'ui.userfront.login.verification.title',
|
||||
);
|
||||
String _verificationTitle = tr('ui.userfront.login.verification.title');
|
||||
String _verificationPageTitle = tr(
|
||||
'ui.userfront.login.verification.page_title',
|
||||
);
|
||||
@@ -127,11 +125,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (!_noticeHandled && notice == 'qr_login_required') {
|
||||
_noticeHandled = true;
|
||||
_showInfo(
|
||||
tr(
|
||||
'msg.userfront.login.qr_login_required',
|
||||
),
|
||||
);
|
||||
_showInfo(tr('msg.userfront.login.qr_login_required'));
|
||||
}
|
||||
|
||||
if (!_verificationOnly) {
|
||||
@@ -234,9 +228,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
);
|
||||
final redirectTo = res['redirectTo'] as String?;
|
||||
if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||
debugPrint("[Auth] OIDC login accepted. Redirecting to: $redirectTo");
|
||||
webWindow.redirectTo(redirectTo);
|
||||
return true;
|
||||
return _redirectToOidcTarget(redirectTo, source: 'accept_oidc_login');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] OIDC login auto-accept failed: $e");
|
||||
@@ -244,6 +236,23 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _redirectToOidcTarget(String redirectTo, {required String source}) {
|
||||
final checked = validateOidcRedirectTarget(redirectTo);
|
||||
debugPrint(
|
||||
"[Auth] OIDC redirect check ($source): valid=${checked.isValid}, reason=${checked.reason}, len=${checked.length}, host=${checked.host}, path=${checked.path}, has_login_verifier=${checked.hasLoginVerifier}",
|
||||
);
|
||||
|
||||
if (!checked.isValid || checked.uri == null) {
|
||||
if (mounted) {
|
||||
_showError(tr('msg.userfront.login.oidc_failed'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
webWindow.redirectTo(checked.uri.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
void _resetLinkLoginState() {
|
||||
_linkPendingRef = null;
|
||||
_lastLinkLoginId = null;
|
||||
@@ -307,9 +316,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
setState(() {
|
||||
_linkExpired = true;
|
||||
});
|
||||
_showInfo(
|
||||
tr('msg.userfront.login.link_timeout'),
|
||||
);
|
||||
_showInfo(tr('msg.userfront.login.link_timeout'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -387,9 +394,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
setState(() {
|
||||
_qrExpired = true;
|
||||
});
|
||||
_showInfo(
|
||||
tr('msg.userfront.login.qr_expired'),
|
||||
);
|
||||
_showInfo(tr('msg.userfront.login.qr_expired'));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -440,9 +445,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_qrExpired = true;
|
||||
});
|
||||
}
|
||||
_showError(
|
||||
tr('msg.userfront.login.qr_expired'),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.qr_expired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -454,11 +457,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (token is String && token.isNotEmpty) {
|
||||
_completeLoginFromToken(token);
|
||||
} else {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.token_missing'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -533,14 +532,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Duration redirectDelay = const Duration(seconds: 2),
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
final resolvedTitle =
|
||||
title ?? tr('ui.userfront.login.verification.title');
|
||||
final resolvedTitle = title ?? tr('ui.userfront.login.verification.title');
|
||||
final resolvedPageTitle =
|
||||
pageTitle ??
|
||||
tr('ui.userfront.login.verification.page_title');
|
||||
pageTitle ?? tr('ui.userfront.login.verification.page_title');
|
||||
final resolvedActionLabel =
|
||||
actionLabel ??
|
||||
tr('ui.userfront.login.verification.action_label');
|
||||
actionLabel ?? tr('ui.userfront.login.verification.action_label');
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
@@ -581,9 +577,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_verificationMessage.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.login.verification.success',
|
||||
)
|
||||
? tr('msg.userfront.login.verification.success')
|
||||
: _verificationMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
@@ -613,9 +607,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
Future<void> _verifyToken(String token) async {
|
||||
debugPrint("[Auth] Starting verification for token: $token");
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
);
|
||||
final approvedMessage = tr('msg.userfront.login.verification.approved');
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
);
|
||||
@@ -675,15 +667,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint(
|
||||
"[Auth] Starting code verification for loginId: $sanitizedLoginId",
|
||||
);
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
);
|
||||
final approvedMessage = tr('msg.userfront.login.verification.approved');
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
);
|
||||
final linkLoginMessage = tr(
|
||||
'msg.userfront.login.link.approved',
|
||||
);
|
||||
final linkLoginMessage = tr('msg.userfront.login.link.approved');
|
||||
try {
|
||||
final res = await AuthProxyService.verifyLoginCode(
|
||||
sanitizedLoginId,
|
||||
@@ -721,12 +709,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_markVerificationApproved(
|
||||
linkLoginMessage,
|
||||
title: tr('ui.userfront.login.link.title'),
|
||||
pageTitle: tr(
|
||||
'ui.userfront.login.link.page_title',
|
||||
),
|
||||
actionLabel: tr(
|
||||
'ui.userfront.login.link.action_label',
|
||||
),
|
||||
pageTitle: tr('ui.userfront.login.link.page_title'),
|
||||
actionLabel: tr('ui.userfront.login.link.action_label'),
|
||||
actionPath: '/signin',
|
||||
autoRedirect: true,
|
||||
);
|
||||
@@ -755,9 +739,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final sanitized = shortCode.trim().toUpperCase();
|
||||
if (sanitized.isEmpty) return;
|
||||
debugPrint("[Auth] Starting short code verification for code: $sanitized");
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
);
|
||||
final approvedMessage = tr('msg.userfront.login.verification.approved');
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
);
|
||||
@@ -829,11 +811,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final input = _passwordLoginIdController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
if (input.isEmpty || password.isEmpty) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.password.missing_credentials',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.password.missing_credentials'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -856,7 +834,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final redirectTo = res['redirectTo'] as String?;
|
||||
|
||||
if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||
webWindow.redirectTo(redirectTo);
|
||||
_redirectToOidcTarget(redirectTo, source: 'password_login');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -938,12 +916,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_dismissOverlays();
|
||||
_showInfo(
|
||||
isEmail
|
||||
? tr(
|
||||
'msg.userfront.login.link_sent_email',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.login.link_sent_phone',
|
||||
),
|
||||
? tr('msg.userfront.login.link_sent_email')
|
||||
: tr('msg.userfront.login.link_sent_phone'),
|
||||
);
|
||||
|
||||
final initialInterval = (interval is int && interval > 0)
|
||||
@@ -1009,11 +983,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (result['error'] == 'expired_token') {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_timeout',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.link_timeout'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1034,11 +1004,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.token_missing'));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1049,9 +1015,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (mounted) {
|
||||
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
|
||||
Navigator.of(context).pop();
|
||||
_showError(
|
||||
tr('msg.userfront.login.link_timeout'),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.link_timeout'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,25 +1088,23 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
);
|
||||
final redirectTo = res['redirectTo'] as String?;
|
||||
if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||
debugPrint("[Auth] OIDC login accepted. Redirecting to: $redirectTo");
|
||||
webWindow.redirectTo(redirectTo);
|
||||
_redirectToOidcTarget(
|
||||
redirectTo,
|
||||
source: 'on_login_success_accept_oidc',
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.oidc_failed',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.oidc_failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final uri = Uri.base;
|
||||
final redirectParam =
|
||||
uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_url'];
|
||||
final hasRedirectParam =
|
||||
redirectParam != null && redirectParam.isNotEmpty;
|
||||
uri.queryParameters['redirect_uri'] ??
|
||||
uri.queryParameters['redirect_url'];
|
||||
final hasRedirectParam = redirectParam != null && redirectParam.isNotEmpty;
|
||||
|
||||
if (WebAuthIntegration.isPopup() || hasRedirectParam) {
|
||||
debugPrint(
|
||||
@@ -1163,14 +1125,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('ui.userfront.login.unregistered.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.login.unregistered.body',
|
||||
),
|
||||
),
|
||||
title: Text(tr('ui.userfront.login.unregistered.title')),
|
||||
content: Text(tr('msg.userfront.login.unregistered.body')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
@@ -1182,9 +1138,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_resetLinkLoginState();
|
||||
context.push('/signup');
|
||||
},
|
||||
child: Text(
|
||||
tr('ui.userfront.login.unregistered.action'),
|
||||
),
|
||||
child: Text(tr('ui.userfront.login.unregistered.action')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1249,9 +1203,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.login.dry_send',
|
||||
),
|
||||
tr('msg.userfront.login.dry_send'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8A6D3B),
|
||||
fontSize: 12,
|
||||
@@ -1267,21 +1219,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.password',
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.link',
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.qr',
|
||||
),
|
||||
),
|
||||
Tab(text: tr('ui.userfront.login.tabs.password')),
|
||||
Tab(text: tr('ui.userfront.login.tabs.link')),
|
||||
Tab(text: tr('ui.userfront.login.tabs.qr')),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -1330,9 +1270,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.action.submit',
|
||||
),
|
||||
tr('ui.userfront.login.action.submit'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1365,16 +1303,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.link.send',
|
||||
),
|
||||
tr('ui.userfront.login.link.send'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.link.helper',
|
||||
),
|
||||
tr('msg.userfront.login.link.helper'),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
@@ -1398,8 +1332,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
setState(_resetLinkLoginState);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
minimumSize: const Size.fromHeight(
|
||||
45,
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.common.refresh')),
|
||||
),
|
||||
@@ -1458,15 +1393,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
suffixText:
|
||||
_linkExpireSeconds > 0
|
||||
? tr(
|
||||
'ui.userfront.login.short_code.expire_time',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkExpireSeconds,
|
||||
),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
? tr(
|
||||
'ui.userfront.login.short_code.expire_time',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkExpireSeconds,
|
||||
),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
maxLength: 6,
|
||||
),
|
||||
@@ -1495,8 +1430,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_verifyShortCode(prefix + digits);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
minimumSize: const Size.fromHeight(
|
||||
45,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
@@ -1549,9 +1485,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
},
|
||||
)
|
||||
: tr(
|
||||
'ui.common.resend',
|
||||
),
|
||||
: tr('ui.common.resend'),
|
||||
),
|
||||
),
|
||||
if (!_lastLinkIsEmail) ...[
|
||||
@@ -1627,8 +1561,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
FilledButton(
|
||||
onPressed: _startQrFlow,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
minimumSize: const Size.fromHeight(
|
||||
45,
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.common.refresh')),
|
||||
),
|
||||
@@ -1679,9 +1614,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.scan_hint',
|
||||
),
|
||||
tr('msg.userfront.login.qr.scan_hint'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
@@ -1691,18 +1624,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
TextButton(
|
||||
onPressed: _startQrFlow,
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.qr.refresh',
|
||||
),
|
||||
tr('ui.userfront.login.qr.refresh'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.load_failed',
|
||||
),
|
||||
tr('msg.userfront.login.qr.load_failed'),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -1716,18 +1645,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
TextButton(
|
||||
onPressed: () => context.push('/forgot-password'),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.forgot_password',
|
||||
),
|
||||
tr('ui.userfront.login.forgot_password'),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.no_account',
|
||||
),
|
||||
tr('msg.userfront.login.no_account'),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
@@ -1735,11 +1660,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.push('/signup'),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.signup',
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.userfront.login.signup')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user