diff --git a/userfront/lib/features/admin/presentation/user_management_screen.dart b/userfront/lib/features/admin/presentation/user_management_screen.dart index f61f94fb..94ea9c57 100644 --- a/userfront/lib/features/admin/presentation/user_management_screen.dart +++ b/userfront/lib/features/admin/presentation/user_management_screen.dart @@ -348,9 +348,8 @@ class _UserManagementScreenState extends State with Single separatorBuilder: (_, __) => const Divider(), itemBuilder: (context, index) { final user = _users[index]; - final userObj = user['user'] ?? {}; // Descope struct structure might vary - // Based on Descope API, user root might have fields directly or inside 'user' - // Go SDK SearchAll returns UserResponse struct. + final userObj = user['user'] ?? {}; // 응답 구조가 케이스마다 다를 수 있음 + // 일부 응답은 최상위 또는 user 하위에 필드를 포함합니다. final loginIDs = (user['loginIds'] as List?) ?? []; final loginId = loginIDs.isNotEmpty ? loginIDs.first.toString() : "Unknown ID"; diff --git a/userfront/lib/features/auth/presentation/approve_qr_screen.dart b/userfront/lib/features/auth/presentation/approve_qr_screen.dart index e3ac8460..1c1f680c 100644 --- a/userfront/lib/features/auth/presentation/approve_qr_screen.dart +++ b/userfront/lib/features/auth/presentation/approve_qr_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:descope/descope.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/services/auth_token_store.dart'; @@ -49,9 +48,8 @@ class _ApproveQrScreenState extends State { void _redirectIfNotLoggedIn() { if (_redirectingToLogin || !mounted) return; final hasStoredToken = AuthTokenStore.getToken() != null; - final hasDescopeSession = Descope.sessionManager.session?.refreshToken.isExpired == false; final usesCookie = AuthTokenStore.usesCookie(); - final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie; + final isLoggedIn = hasStoredToken || usesCookie; if (!isLoggedIn) { _redirectingToLogin = true; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -65,13 +63,12 @@ class _ApproveQrScreenState extends State { if (widget.pendingRef == null) return; final storedToken = AuthTokenStore.getToken(); - final session = Descope.sessionManager.session; final usesCookie = AuthTokenStore.usesCookie(); var hasCookie = usesCookie; - if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) { + if (storedToken == null && !hasCookie) { hasCookie = await _bootstrapCookieSession(); } - if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) { + if (storedToken == null && !hasCookie) { if (mounted) { context.go('/signin?notice=qr_login_required'); } @@ -84,7 +81,7 @@ class _ApproveQrScreenState extends State { }); // jwt 유효성 확인 try { - final token = storedToken ?? session?.sessionToken.jwt ?? ''; + final token = storedToken ?? ''; await AuthProxyService.approveQrLogin( widget.pendingRef!, token: token, @@ -109,9 +106,8 @@ class _ApproveQrScreenState extends State { @override Widget build(BuildContext context) { final hasStoredToken = AuthTokenStore.getToken() != null; - final hasDescopeSession = Descope.sessionManager.session?.refreshToken.isExpired == false; final usesCookie = AuthTokenStore.usesCookie(); - final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie || _isCheckingSession; + final isLoggedIn = hasStoredToken || usesCookie || _isCheckingSession; if (!isLoggedIn && !_redirectingToLogin) { _redirectIfNotLoggedIn(); diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index fd258e99..f2730a8e 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:descope/descope.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -191,7 +190,7 @@ class _LoginScreenState extends ConsumerState }); } - // Helper to decode JWT and get loginId + // JWT를 디코딩해 표시용 로그인 아이디 추출 String _getLoginIdFromJwt(String jwt) { try { final parts = jwt.split('.'); @@ -199,7 +198,7 @@ class _LoginScreenState extends ConsumerState final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); final data = json.decode(payload); - // Descope tokens usually have 'name', 'email', or 'sub' + // 일반적으로 name/email/sub 필드를 사용 return data['name'] ?? data['email'] ?? data['sub'] ?? 'User'; } catch (e) { debugPrint("[JWT] Decode error: $e"); @@ -207,20 +206,6 @@ class _LoginScreenState extends ConsumerState } } - // Helper to decode JWT and get User ID (sub claim) - String _getUserIdFromJwt(String jwt) { - try { - final parts = jwt.split('.'); - if (parts.length != 3) return 'unknown'; - final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); - final data = json.decode(payload) as Map; - return data['sub'] as String? ?? 'unknown'; - } catch (e) { - debugPrint("[JWT] Could not extract User ID (sub): $e"); - return 'unknown'; - } - } - void _handleTabSelection() { // QR 탭 (세 번째 탭, index 2)이 선택되었을 때 QR 플로우 시작 if (_tabController.index == 2 && _qrPendingRef == null) { @@ -350,12 +335,7 @@ class _LoginScreenState extends ConsumerState }) { final isJwt = token.split('.').length == 3; if (isJwt) { - final displayName = _getLoginIdFromJwt(token); - final dummyUser = DescopeUser( - 'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [], - ); - final session = DescopeSession.fromJwt(token, token, dummyUser); - Descope.sessionManager.manageSession(session); + _getLoginIdFromJwt(token); } if (!mounted) return; @@ -813,33 +793,14 @@ class _LoginScreenState extends ConsumerState _logTokenDetails(token); - final userId = _getUserIdFromJwt(token); final providerName = provider ?? AuthTokenStore.getProvider(); - final isJwt = token.split('.').length == 3; - final isOry = (providerName ?? '').toLowerCase().contains('ory') || !isJwt; AuthTokenStore.setToken(token, provider: providerName); AuthTokenStore.clearPendingProvider(); - // [New] 로그인 성공 직후 백엔드에서 전체 프로필 정보를 가져와 세션 업데이트 + // 로그인 성공 직후 백엔드에서 전체 프로필 정보를 가져와 세션 업데이트 try { - if (!isOry) { - // 임시 세션 생성 (API 호출을 위해) - final tempUser = DescopeUser(userId, [], 0, 'User', null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], []); - final tempSession = DescopeSession.fromJwt(token, token, tempUser); - Descope.sessionManager.manageSession(tempSession); - } - - // 백엔드 GetMe 호출 (프로필 노티파이어 사용) - final profile = await ref.read(profileProvider.notifier).loadProfile(); - if (profile != null && !isOry) { - // 실제 정보로 세션 유저 정보 교체 - final realUser = DescopeUser( - userId, [], 0, profile.name, null, profile.email, false, profile.phone, false, {}, '', '', '', false, 'enabled', [], [], [], - ); - final realSession = DescopeSession.fromJwt(token, token, realUser); - Descope.sessionManager.manageSession(realSession); - } + await ref.read(profileProvider.notifier).loadProfile(); } catch (e) { debugPrint("[Auth] Failed to pre-fetch profile: $e"); } diff --git a/userfront/lib/features/auth/presentation/qr_scan_screen.dart b/userfront/lib/features/auth/presentation/qr_scan_screen.dart index 805db385..9586a2fe 100644 --- a/userfront/lib/features/auth/presentation/qr_scan_screen.dart +++ b/userfront/lib/features/auth/presentation/qr_scan_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; -import 'package:descope/descope.dart'; import '../../../core/services/auth_proxy_service.dart'; import '../../../core/services/auth_token_store.dart'; @@ -94,7 +93,7 @@ class _QRScanScreenState extends State { final approveRef = qrData; final storedToken = AuthTokenStore.getToken(); - final sessionToken = storedToken ?? Descope.sessionManager.session?.sessionToken.jwt; + final sessionToken = storedToken; var usesCookie = AuthTokenStore.usesCookie(); if (sessionToken == null && !usesCookie) { usesCookie = await _bootstrapCookieSession(); diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 3372e0fa..10752bed 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:descope/descope.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../../../core/notifiers/auth_notifier.dart'; @@ -147,7 +146,6 @@ class _DashboardScreenState extends ConsumerState { } Future _logout() async { - Descope.sessionManager.clearSession(); AuthTokenStore.clear(); AuthNotifier.instance.notify(); } @@ -334,9 +332,6 @@ class _DashboardScreenState extends ConsumerState { if (lower.contains('ory')) { return 'Ory 세션'; } - if (lower.contains('descope')) { - return 'Descope'; - } return provider; } @@ -450,11 +445,7 @@ class _DashboardScreenState extends ConsumerState { final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint; final profileState = ref.watch(profileProvider); final profile = profileState.value; - final user = Descope.sessionManager.session?.user; - final userName = user?.name ?? - user?.email ?? - user?.phone ?? - profile?.name ?? + final userName = profile?.name ?? profile?.email ?? profile?.phone ?? 'User'; diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index d3a657d1..1e86f822 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -1,4 +1,3 @@ -import 'package:descope/descope.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -43,6 +42,55 @@ class _ProfilePageState extends ConsumerState { bool _isCodeSent = false; bool _isVerifying = false; + @override + void initState() { + super.initState(); + _nameFocus.addListener(_onNameFocusChange); + _departmentFocus.addListener(_onDepartmentFocusChange); + _phoneFocus.addListener(_onPhoneFocusChange); + _phoneCodeFocus.addListener(_onPhoneCodeFocusChange); + } + + void _onNameFocusChange() { + if (!mounted) return; + if (!_nameFocus.hasFocus && _nameTouched) { + final profile = ref.read(profileProvider).value ?? _cachedProfile; + if (profile != null) _autoSaveIfEditing(profile, 'name'); + } else if (_nameFocus.hasFocus) { + _nameTouched = true; + } + } + + void _onDepartmentFocusChange() { + if (!mounted) return; + if (!_departmentFocus.hasFocus && _departmentTouched) { + final profile = ref.read(profileProvider).value ?? _cachedProfile; + if (profile != null) _autoSaveIfEditing(profile, 'department'); + } else if (_departmentFocus.hasFocus) { + _departmentTouched = true; + } + } + + void _onPhoneFocusChange() { + if (!mounted) return; + if (!_phoneFocus.hasFocus && _phoneTouched) { + final profile = ref.read(profileProvider).value ?? _cachedProfile; + if (profile != null) _handlePhoneFocusChange(profile); + } else if (_phoneFocus.hasFocus) { + _phoneTouched = true; + } + } + + void _onPhoneCodeFocusChange() { + if (!mounted) return; + if (!_phoneCodeFocus.hasFocus && _phoneCodeTouched) { + final profile = ref.read(profileProvider).value ?? _cachedProfile; + if (profile != null) _handlePhoneFocusChange(profile); + } else if (_phoneCodeFocus.hasFocus) { + _phoneCodeTouched = true; + } + } + @override void dispose() { _nameController?.dispose(); @@ -57,7 +105,6 @@ class _ProfilePageState extends ConsumerState { } Future _logout() async { - Descope.sessionManager.clearSession(); AuthTokenStore.clear(); AuthNotifier.instance.notify(); } @@ -532,40 +579,14 @@ class _ProfilePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( - child: Focus( + child: TextField( + controller: controller, focusNode: field == 'name' ? _nameFocus : _departmentFocus, - onFocusChange: (hasFocus) { - if (field == 'name') { - if (hasFocus) { - _nameTouched = true; - return; - } - if (!_nameTouched) { - return; - } - } - if (field == 'department') { - if (hasFocus) { - _departmentTouched = true; - return; - } - if (!_departmentTouched) { - return; - } - } - if (!hasFocus) { - _autoSaveIfEditing(profile, field); - } - }, - child: TextField( - controller: controller, - focusNode: field == 'name' ? _nameFocus : _departmentFocus, - textInputAction: TextInputAction.done, - onSubmitted: (_) => _autoSaveIfEditing(profile, field), - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: label, - ), + textInputAction: TextInputAction.done, + onSubmitted: (_) => _autoSaveIfEditing(profile, field), + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: label, ), ), ), @@ -605,35 +626,20 @@ class _ProfilePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( - child: Focus( + child: TextField( + controller: _phoneController, focusNode: _phoneFocus, - onFocusChange: (hasFocus) { - if (hasFocus) { - _phoneTouched = true; - return; - } - if (!_phoneTouched) { - return; - } - if (!hasFocus) { - _handlePhoneFocusChange(profile); - } - }, - child: TextField( - controller: _phoneController, - focusNode: _phoneFocus, - keyboardType: TextInputType.phone, - textInputAction: TextInputAction.done, - onSubmitted: (_) => _autoSaveIfEditing(profile, 'phone'), - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: '01012345678', - suffixIcon: _isPhoneVerified - ? const Icon(Icons.check_circle, color: Colors.green) - : null, - ), - enabled: !_isPhoneVerified, + keyboardType: TextInputType.phone, + textInputAction: TextInputAction.done, + onSubmitted: (_) => _autoSaveIfEditing(profile, 'phone'), + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: '01012345678', + suffixIcon: _isPhoneVerified + ? const Icon(Icons.check_circle, color: Colors.green) + : null, ), + enabled: !_isPhoneVerified, ), ), const SizedBox(width: 8), @@ -655,30 +661,15 @@ class _ProfilePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( - child: Focus( + child: TextField( + controller: _codeController, focusNode: _phoneCodeFocus, - onFocusChange: (hasFocus) { - if (hasFocus) { - _phoneCodeTouched = true; - return; - } - if (!_phoneCodeTouched) { - return; - } - if (!hasFocus) { - _handlePhoneFocusChange(profile); - } - }, - child: TextField( - controller: _codeController, - focusNode: _phoneCodeFocus, - keyboardType: TextInputType.number, - textInputAction: TextInputAction.done, - onSubmitted: (_) => _verifyCode(profile), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: '인증번호 6자리', - ), + keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + onSubmitted: (_) => _verifyCode(profile), + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: '인증번호 6자리', ), ), ), diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index 78434059..e87146b4 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:descope/descope.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; @@ -66,21 +65,6 @@ void main() async { // 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화 await _loadBundledFonts(); - // Initialize Descope (프로젝트 ID가 없으면 경고만 남기고 진행) - final projectId = dotenv.maybeGet('DESCOPE_PROJECT_ID') ?? ''; - if (projectId.isEmpty || projectId == 'your-project-id') { - _log.severe("DESCOPE_PROJECT_ID is missing. Descope may not work correctly."); - } - Descope.setup(projectId); - - // Load saved session if any - try { - // 저장된 세션 불러옴 - await Descope.sessionManager.loadSession(); - } catch (e) { - _log.warning("Failed to load session: $e"); - } - runApp(const ProviderScope(child: BaronSSOApp())); } @@ -189,11 +173,9 @@ final _router = GoRouter( ), ], redirect: (context, state) { - final hasDescopeSession = - Descope.sessionManager.session?.refreshToken?.isExpired == false; final hasStoredToken = AuthTokenStore.getToken() != null; final hasCookieSession = AuthTokenStore.usesCookie(); - final isLoggedIn = hasDescopeSession || hasStoredToken || hasCookieSession; + final isLoggedIn = hasStoredToken || hasCookieSession; final path = state.uri.path; // Public paths that don't require login diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index 3e6488da..77ba4108 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -97,14 +97,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - cryptography: - dependency: transitive - description: - name: cryptography - sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" - url: "https://pub.dev" - source: hosted - version: "2.9.0" cupertino_icons: dependency: "direct main" description: @@ -113,14 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - descope: - dependency: "direct main" - description: - name: descope - sha256: cae7d22e47d7d1c35d5a1cdfad2ae5f3b5e755476f215ddfa4dbeab6937f3ac2 - url: "https://pub.dev" - source: hosted - version: "0.9.12" fake_async: dependency: transitive description: @@ -129,14 +113,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" file: dependency: transitive description: @@ -248,14 +224,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" - url: "https://pub.dev" - source: hosted - version: "4.10.0" leak_tracker: dependency: transitive description: diff --git a/userfront/pubspec.yaml b/userfront/pubspec.yaml index 0be958f8..bdef42b8 100644 --- a/userfront/pubspec.yaml +++ b/userfront/pubspec.yaml @@ -1,4 +1,4 @@ -name: app.brsw.kr +name: userfront description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. @@ -36,7 +36,6 @@ dependencies: cupertino_icons: ^1.0.8 flutter_riverpod: ^3.0.3 go_router: ^17.0.1 - descope: ^0.9.11 http: ^1.6.0 flutter_dotenv: ^6.0.0 url_launcher: ^6.3.2