1
0
forked from baron/baron-sso

fix userfront mobile approval close flow

This commit is contained in:
2026-05-21 18:14:31 +09:00
parent 66687a4c73
commit e54cc121c7
5 changed files with 100 additions and 30 deletions

View File

@@ -160,6 +160,14 @@ function collectClientFailures(page: Page): string[] {
return failures; return failures;
} }
async function makeWindowCloseNavigateToRoot(page: Page): Promise<void> {
await page.addInitScript(() => {
window.close = () => {
window.location.href = '/';
};
});
}
test.describe('UserFront WASM auth routing', () => { test.describe('UserFront WASM auth routing', () => {
test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => { test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => {
await mockUserfrontApis(page, { sessionStatus: 401 }); await mockUserfrontApis(page, { sessionStatus: 401 });
@@ -239,6 +247,7 @@ test.describe('UserFront WASM auth routing', () => {
verifyRequests.push({ path, body }); verifyRequests.push({ path, body });
}, },
}); });
await makeWindowCloseNavigateToRoot(page);
await page.goto('/ko/l/AB123456'); await page.goto('/ko/l/AB123456');
@@ -281,6 +290,7 @@ test.describe('UserFront WASM auth routing', () => {
verifyCalls += 1; verifyCalls += 1;
}, },
}); });
await makeWindowCloseNavigateToRoot(page);
await page.goto('/ko/l/AB123456'); await page.goto('/ko/l/AB123456');
@@ -375,6 +385,7 @@ test.describe('UserFront WASM auth routing', () => {
verifyRequests.push({ path, body }); verifyRequests.push({ path, body });
}, },
}); });
await makeWindowCloseNavigateToRoot(page);
await page.goto('/ko/verify/e2e-email-token'); await page.goto('/ko/verify/e2e-email-token');
@@ -423,6 +434,7 @@ test.describe('UserFront WASM auth routing', () => {
verifyRequests.push({ path, body }); verifyRequests.push({ path, body });
}, },
}); });
await makeWindowCloseNavigateToRoot(page);
await page.goto( await page.goto(
'/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email', '/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email',

View File

@@ -232,6 +232,7 @@ body = "We could not find an account for that information.\\\\\\\\\\\\\\\\nPleas
approved = "Approved. Complete sign-in in the original window." approved = "Approved. Complete sign-in in the original window."
approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly." approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly."
approved_remote = "Approved. Please return to the original browser or PC screen." approved_remote = "Approved. Please return to the original browser or PC screen."
pending_remote = "Checking the sign-in approval request. Please wait."
success = "Sign-in approval completed." success = "Sign-in approval completed."
[msg.userfront.login_success] [msg.userfront.login_success]
@@ -584,6 +585,7 @@ action_label = "Done"
action_label_close = "Close Window" action_label_close = "Close Window"
page_title = "Sign-in approval" page_title = "Sign-in approval"
title = "Approval complete" title = "Approval complete"
title_pending = "Checking approval"
title_remote = "Sign-in approved" title_remote = "Sign-in approved"
[ui.userfront.login_success] [ui.userfront.login_success]
@@ -702,4 +704,3 @@ toggle_label = "Show active sessions only"
[msg.userfront.audit.filter] [msg.userfront.audit.filter]
description = "Toggle to view only active sessions." description = "Toggle to view only active sessions."

View File

@@ -456,6 +456,7 @@ body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다." approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다" approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요." approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요."
pending_remote = "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요."
success = "로그인 승인에 성공했습니다." success = "로그인 승인에 성공했습니다."
[msg.userfront.login_success] [msg.userfront.login_success]
@@ -807,6 +808,7 @@ page_title = "로그인 승인"
title = "승인 완료" title = "승인 완료"
action_label_close = "창 닫기" action_label_close = "창 닫기"
title_remote = "로그인 승인 완료" title_remote = "로그인 승인 완료"
title_pending = "로그인 승인 확인 중"
[ui.userfront.login_success] [ui.userfront.login_success]
later = "나중에 하기 (대시보드로 이동)" later = "나중에 하기 (대시보드로 이동)"
@@ -923,4 +925,3 @@ toggle_label = "활성 세션만 보기"
[msg.userfront.audit.filter] [msg.userfront.audit.filter]
description = "활성화된 세션만 보려면 토글을 켜주세요." description = "활성화된 세션만 보려면 토글을 켜주세요."

View File

@@ -100,6 +100,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_redirectUrl = widget.redirectUrl; _redirectUrl = widget.redirectUrl;
_passwordFocusNode.addListener(_handlePasswordFocusChange); _passwordFocusNode.addListener(_handlePasswordFocusChange);
HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent); HardwareKeyboard.instance.addHandler(_handleHardwareKeyEvent);
_verificationOnly = _isVerificationOnlyUri(Uri.base);
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final uri = Uri.base; final uri = Uri.base;
@@ -127,8 +128,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final hasVerificationToken = final hasVerificationToken =
widget.verificationToken != null || hasTokenParam; widget.verificationToken != null || hasTokenParam;
final hasLoginCode = loginIdParam != null && codeParam != null; final hasLoginCode = loginIdParam != null && codeParam != null;
_verificationOnly =
hasVerificationToken || hasLoginCode || hasShortCodePath;
final notice = uri.queryParameters['notice']; final notice = uri.queryParameters['notice'];
if (hasShortCodePath) { if (hasShortCodePath) {
@@ -174,6 +173,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}); });
} }
bool _isVerificationOnlyUri(Uri uri) {
final loginIdParam = uri.queryParameters['loginId'];
final codeParam = uri.queryParameters['code'];
return widget.verificationToken != null ||
uri.queryParameters.containsKey('t') ||
(loginIdParam != null && codeParam != null) ||
extractLoginShortCode(uri) != null;
}
void _handlePasswordFocusChange() { void _handlePasswordFocusChange() {
if (!mounted) { if (!mounted) {
return; return;
@@ -765,6 +773,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_onVerificationAction?.call(); _onVerificationAction?.call();
} }
void _closeVerificationWindowIfPossible() {
if (webWindow.hasOpener()) {
webWindow.close();
}
}
Widget _buildVerificationResultView() { Widget _buildVerificationResultView() {
return Center( return Center(
child: Padding( child: Padding(
@@ -802,7 +816,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return; return;
} }
if (_verificationOnly) { if (_verificationOnly) {
webWindow.close(); _closeVerificationWindowIfPossible();
return; return;
} }
final hasLocalSession = final hasLocalSession =
@@ -827,6 +841,55 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
); );
} }
Widget _buildVerificationPendingView() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(strokeWidth: 4),
),
const SizedBox(height: 20),
Text(
tr('ui.userfront.login.verification.title_pending'),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text(
tr('msg.userfront.login.verification.pending_remote'),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.black54),
),
],
),
),
);
}
Widget _buildVerificationOnlyScaffold() {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_verificationPageTitle),
leading: _verificationApproved && _onVerificationAction != null
? IconButton(
icon: const Icon(Icons.close),
onPressed: _runVerificationExitAction,
)
: null,
actions: const [ThemeToggleButton(compact: true)],
),
body: _verificationApproved
? _buildVerificationResultView()
: _buildVerificationPendingView(),
);
}
Future<void> _verifyToken(String token) async { Future<void> _verifyToken(String token) async {
debugPrint("[Auth] Starting verification for token: $token"); debugPrint("[Auth] Starting verification for token: $token");
final approvedMessage = tr('msg.userfront.login.verification.approved'); final approvedMessage = tr('msg.userfront.login.verification.approved');
@@ -858,7 +921,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -894,7 +957,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -951,7 +1014,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -972,7 +1035,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
return; return;
} }
@@ -985,7 +1048,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
remoteApprovedMessage, remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'), title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'), actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
} catch (e) { } catch (e) {
@@ -1005,7 +1068,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -1053,7 +1116,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -1074,7 +1137,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
return; return;
} }
@@ -1087,7 +1150,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
remoteApprovedMessage, remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'), title: tr('ui.userfront.login.verification.title_remote'),
actionLabel: tr('ui.userfront.login.verification.action_label_close'), actionLabel: tr('ui.userfront.login.verification.action_label_close'),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
} catch (e) { } catch (e) {
@@ -1105,7 +1168,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
actionLabel: tr( actionLabel: tr(
'ui.userfront.login.verification.action_label_close', 'ui.userfront.login.verification.action_label_close',
), ),
onAction: () => webWindow.close(), onAction: _closeVerificationWindowIfPossible,
); );
} }
return; return;
@@ -1609,21 +1672,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
); );
if (_verificationOnly && _verificationApproved) { if (_verificationOnly) {
return Scaffold( return _buildVerificationOnlyScaffold();
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_verificationPageTitle),
leading: _onVerificationAction == null
? null
: IconButton(
icon: const Icon(Icons.close),
onPressed: _runVerificationExitAction,
),
actions: const [ThemeToggleButton(compact: true)],
),
body: _buildVerificationResultView(),
);
} }
return Scaffold( return Scaffold(

View File

@@ -649,6 +649,8 @@ const Map<String, String> koStrings = {
"승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요.", "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요.",
"msg.userfront.login.verification.approved_local": "msg.userfront.login.verification.approved_local":
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
"msg.userfront.login.verification.pending_remote":
"승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요.",
"msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.", "msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.",
"msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}", "msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}",
"msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.", "msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.",
@@ -1919,6 +1921,7 @@ const Map<String, String> koStrings = {
"ui.userfront.login.verification.action_label_close": "창 닫기", "ui.userfront.login.verification.action_label_close": "창 닫기",
"ui.userfront.login.verification.page_title": "로그인 승인", "ui.userfront.login.verification.page_title": "로그인 승인",
"ui.userfront.login.verification.title": "승인 완료", "ui.userfront.login.verification.title": "승인 완료",
"ui.userfront.login.verification.title_pending": "로그인 승인 확인 중",
"ui.userfront.login.verification.title_remote": "로그인 승인 완료", "ui.userfront.login.verification.title_remote": "로그인 승인 완료",
"ui.userfront.login_success.later": "나중에 하기 (대시보드로 이동)", "ui.userfront.login_success.later": "나중에 하기 (대시보드로 이동)",
"ui.userfront.login_success.qr": "QR 인증 (카메라 켜기)", "ui.userfront.login_success.qr": "QR 인증 (카메라 켜기)",
@@ -2785,6 +2788,8 @@ const Map<String, String> enStrings = {
"Approved. Please return to the original browser or PC screen.", "Approved. Please return to the original browser or PC screen.",
"msg.userfront.login.verification.approved_local": "msg.userfront.login.verification.approved_local":
"Approved. This device is already signed in, and the remote window will be signed in shortly.", "Approved. This device is already signed in, and the remote window will be signed in shortly.",
"msg.userfront.login.verification.pending_remote":
"Checking the sign-in approval request. Please wait.",
"msg.userfront.login.verification.success": "Sign-in approval completed.", "msg.userfront.login.verification.success": "Sign-in approval completed.",
"msg.userfront.login.verification_failed": "msg.userfront.login.verification_failed":
"Failed to approve the sign-in request: {{error}}", "Failed to approve the sign-in request: {{error}}",
@@ -4119,6 +4124,7 @@ const Map<String, String> enStrings = {
"ui.userfront.login.verification.action_label_close": "Close Window", "ui.userfront.login.verification.action_label_close": "Close Window",
"ui.userfront.login.verification.page_title": "Sign-in approval", "ui.userfront.login.verification.page_title": "Sign-in approval",
"ui.userfront.login.verification.title": "Approval complete", "ui.userfront.login.verification.title": "Approval complete",
"ui.userfront.login.verification.title_pending": "Checking approval",
"ui.userfront.login.verification.title_remote": "Sign-in approved", "ui.userfront.login.verification.title_remote": "Sign-in approved",
"ui.userfront.login_success.later": "Do this later (go to dashboard)", "ui.userfront.login_success.later": "Do this later (go to dashboard)",
"ui.userfront.login_success.qr": "Use QR approval", "ui.userfront.login_success.qr": "Use QR approval",