diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts index 79e4a0b3..a9e6b2e2 100644 --- a/userfront-e2e/tests/auth-routing.spec.ts +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -95,10 +95,14 @@ async function mockUserfrontApis( pendingRef?: string; }; pendingRef = body.pendingRef ?? null; - } catch (_) { + console.log(`[E2E-MOCK] /api/v1/auth/qr/approve POST body:`, body); + } catch (e) { + console.log(`[E2E-MOCK] /api/v1/auth/qr/approve POST body parse error:`, e); pendingRef = null; } options.captureApprove?.(pendingRef); + } else { + console.log(`[E2E-MOCK] /api/v1/auth/qr/approve ${route.request().method()} request`); } await route.fulfill({ status: 200, diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index ac1a801c..1a540f24 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -243,21 +243,13 @@ final _router = GoRouter( routes: [ GoRoute( path: '/:locale', - redirect: (context, state) { - // /{locale} 진입은 화면 렌더링 없이 단일 목적지로만 보냅니다. - if (state.uri.pathSegments.length != 1) { - return null; - } + builder: (context, state) { final rawLocale = state.pathParameters['locale']; final localeCode = normalizeLocaleCode(rawLocale); - final token = AuthTokenStore.getToken(); - final isLoggedIn = - (token != null && token.isNotEmpty) || - AuthTokenStore.usesCookie(); - if (!isLoggedIn) { - return buildSigninRedirectPath(localeCode, state.uri); - } - return '/$localeCode/dashboard'; + return ScopedTheme( + controller: ThemeController.auth, + child: LocaleEntryRedirectScreen(localeCode: localeCode), + ); }, routes: [ GoRoute( @@ -478,6 +470,10 @@ final _router = GoRouter( (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie(); final path = stripLocalePath(uri); + if (!isLoggedIn && (path == '/approve' || path.startsWith('/ql/'))) { + return '/$requestedLocale/signin?notice=qr_login_required'; + } + final isPublicPath = isPublicAuthPath(path, uri); if (isPublicPath) { @@ -499,9 +495,24 @@ final _router = GoRouter( }, ); -class BaronSSOApp extends StatelessWidget { +class BaronSSOApp extends StatefulWidget { const BaronSSOApp({super.key}); + @override + State createState() => _BaronSSOAppState(); +} + +class _BaronSSOAppState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Re-run router redirects after the first frame so session-only web + // storage state is reflected even when startup routing evaluated too early. + AuthNotifier.instance.notify(); + }); + } + @override Widget build(BuildContext context) { final localization = EasyLocalization.of(context); @@ -531,3 +542,45 @@ class BaronSSOApp extends StatelessWidget { ); } } + +class LocaleEntryRedirectScreen extends StatefulWidget { + const LocaleEntryRedirectScreen({super.key, required this.localeCode}); + + final String localeCode; + + @override + State createState() => + _LocaleEntryRedirectScreenState(); +} + +class _LocaleEntryRedirectScreenState extends State { + bool _redirected = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _redirect(); + }); + } + + void _redirect() { + if (!mounted || _redirected) { + return; + } + _redirected = true; + final token = AuthTokenStore.getToken(); + final isLoggedIn = + (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie(); + if (!isLoggedIn) { + context.go('/${widget.localeCode}/signin'); + return; + } + context.go('/${widget.localeCode}/dashboard'); + } + + @override + Widget build(BuildContext context) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } +}