forked from baron/baron-sso
로그인 이력확인
This commit is contained in:
@@ -12,6 +12,13 @@ class ForgotPasswordScreen extends StatefulWidget {
|
||||
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
final TextEditingController _loginIdController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _drySendEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_drySendEnabled = _parseBoolParam(Uri.base.queryParameters['drySend']) && !AuthProxyService.isProdEnv;
|
||||
}
|
||||
|
||||
Future<void> _handlePasswordReset() async {
|
||||
final input = _loginIdController.text.trim();
|
||||
@@ -32,7 +39,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
await AuthProxyService.initiatePasswordReset(loginId);
|
||||
await AuthProxyService.initiatePasswordReset(loginId, drySend: _drySendEnabled);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -59,6 +66,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
bool _parseBoolParam(String? value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
final normalized = value.toLowerCase();
|
||||
return normalized == 'true' || normalized == '1' || normalized == 'yes';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -82,6 +97,29 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (_drySendEnabled) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3CD),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFFC107)),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.",
|
||||
style: TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.",
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -47,6 +46,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Timer? _linkResendTimer;
|
||||
int _linkExpireSeconds = 0;
|
||||
Timer? _linkExpireTimer;
|
||||
bool _verificationOnly = false;
|
||||
bool _drySendEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -54,6 +55,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
// 탭 컨트롤러: 3개 탭, 기본 선택은 두 번째 탭("로그인 링크")
|
||||
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
|
||||
_tabController.addListener(_handleTabSelection);
|
||||
_drySendEnabled = _parseBoolParam(Uri.base.queryParameters['drySend']) && !AuthProxyService.isProdEnv;
|
||||
|
||||
// Check for tokens (Path Parameter or Legacy Query Parameter)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -61,19 +63,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final loginIdParam = uri.queryParameters['loginId'];
|
||||
final codeParam = uri.queryParameters['code'];
|
||||
final pendingRefParam = uri.queryParameters['pendingRef'];
|
||||
if (uri.pathSegments.length >= 2 && uri.pathSegments.first == 'l') {
|
||||
final hasShortCodePath = uri.pathSegments.length >= 2 && uri.pathSegments.first == 'l';
|
||||
final hasTokenParam = uri.queryParameters.containsKey('t');
|
||||
final hasVerificationToken = widget.verificationToken != null || hasTokenParam;
|
||||
final hasLoginCode = loginIdParam != null && codeParam != null;
|
||||
_verificationOnly = hasVerificationToken || hasLoginCode || hasShortCodePath;
|
||||
|
||||
if (hasShortCodePath) {
|
||||
final shortCode = uri.pathSegments[1];
|
||||
_verifyShortCode(shortCode);
|
||||
}
|
||||
if (loginIdParam != null && codeParam != null) {
|
||||
if (hasLoginCode) {
|
||||
_verifyLoginCode(loginIdParam, codeParam, pendingRef: pendingRefParam);
|
||||
} else if (widget.verificationToken != null) {
|
||||
_verifyToken(widget.verificationToken!);
|
||||
} else if (uri.queryParameters.containsKey('t')) {
|
||||
_verifyToken(uri.queryParameters['t']!);
|
||||
} else if (hasVerificationToken) {
|
||||
_verifyToken(widget.verificationToken ?? uri.queryParameters['t']!);
|
||||
}
|
||||
|
||||
_tryCookieSession();
|
||||
if (!_verificationOnly) {
|
||||
_tryCookieSession();
|
||||
}
|
||||
|
||||
if (uri.queryParameters.containsKey('redirect_url')) {
|
||||
_redirectUrl = uri.queryParameters['redirect_url'];
|
||||
@@ -128,6 +136,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_shortCodeDigitsController.clear();
|
||||
}
|
||||
|
||||
bool _parseBoolParam(String? value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
final normalized = value.toLowerCase();
|
||||
return normalized == 'true' || normalized == '1' || normalized == 'yes';
|
||||
}
|
||||
|
||||
void _startLinkResendTimer(int seconds) {
|
||||
_linkResendSeconds = seconds;
|
||||
_linkResendTimer?.cancel();
|
||||
@@ -284,40 +300,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
if (res['status'] == 'ok' && res['sessionJwt'] != null) {
|
||||
if (res['status'] == 'ok') {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
|
||||
final token = res['sessionJwt'] as String;
|
||||
final isJwt = token.split('.').length == 3;
|
||||
if (isJwt) {
|
||||
final displayName = _getLoginIdFromJwt(token);
|
||||
// Create User & Session for Descope SDK
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', // userId
|
||||
[], // loginIds
|
||||
0, // createdAt
|
||||
displayName, // name
|
||||
null, // picture (Uri?)
|
||||
'', // email
|
||||
false, // isVerifiedEmail
|
||||
'', // phone
|
||||
false, // isVerifiedPhone
|
||||
{}, // customAttributes
|
||||
'', // givenName
|
||||
'', // middleName
|
||||
'', // familyName
|
||||
false, // hasPassword
|
||||
'enabled', // status
|
||||
[], // roleNames
|
||||
[], // ssoAppIds
|
||||
[], // oauthProviders (List<String>)
|
||||
);
|
||||
final session = DescopeSession.fromJwt(token, token, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
final token = res['sessionJwt'] ?? res['token'];
|
||||
if (token is String && token.isNotEmpty) {
|
||||
_completeLoginFromToken(token);
|
||||
} else {
|
||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
||||
}
|
||||
|
||||
_onLoginSuccess(token);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[QR] Polling error: $e");
|
||||
@@ -339,26 +330,37 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return "${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
void _completeLoginFromToken(
|
||||
String token, {
|
||||
String? provider,
|
||||
bool closeDialog = false,
|
||||
}) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
if (closeDialog && Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_onLoginSuccess(token, provider: provider);
|
||||
}
|
||||
|
||||
Future<void> _verifyToken(String token) async {
|
||||
debugPrint("[Auth] Starting verification for token: $token");
|
||||
try {
|
||||
// Use Backend to verify the token (Backend-Driven Flow)
|
||||
final res = await AuthProxyService.verifyMagicLink(token);
|
||||
final jwt = res['token'];
|
||||
await AuthProxyService.verifyMagicLink(token);
|
||||
debugPrint("[Auth] Verification successful for token: $token");
|
||||
|
||||
if (jwt != null && mounted) {
|
||||
final displayName = _getLoginIdFromJwt(jwt);
|
||||
// Create User & Session for Descope SDK to log in this tab
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
// Refresh Token을 LocalStorage에 저장
|
||||
Descope.sessionManager.manageSession(session);
|
||||
|
||||
// Notify and Go to Dashboard
|
||||
_onLoginSuccess(jwt);
|
||||
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
||||
@@ -388,17 +390,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt != null && mounted) {
|
||||
final isJwt = (jwt as String).split('.').length == 3;
|
||||
if (isJwt) {
|
||||
final displayName = _getLoginIdFromJwt(jwt);
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
if (_verificationOnly) {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
_onLoginSuccess(jwt, provider: res['provider'] as String?);
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
||||
@@ -425,17 +425,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt != null && mounted) {
|
||||
final isJwt = (jwt as String).split('.').length == 3;
|
||||
if (isJwt) {
|
||||
final displayName = _getLoginIdFromJwt(jwt);
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
if (_verificationOnly) {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
_onLoginSuccess(jwt, provider: res['provider'] as String?);
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Short code verification FAILED. Error: $e");
|
||||
@@ -543,6 +541,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final initResponse = await AuthProxyService.initEnchantedLink(
|
||||
loginId,
|
||||
codeOnly: codeOnly,
|
||||
drySend: _drySendEnabled,
|
||||
);
|
||||
final pendingRef = initResponse['pendingRef'];
|
||||
final mode = (initResponse['mode'] ?? '').toString();
|
||||
@@ -627,24 +626,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
|
||||
if (result['status'] == 'ok') {
|
||||
final jwt = result['sessionJwt'];
|
||||
if (jwt != null) {
|
||||
final token = result['sessionJwt'] ?? result['token'];
|
||||
if (token is String && token.isNotEmpty) {
|
||||
debugPrint("[Auth] Polling SUCCESS. Token received.");
|
||||
|
||||
final displayName = _getLoginIdFromJwt(jwt);
|
||||
// Descope SDK 세션 강제 주입
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
_completeLoginFromToken(
|
||||
token,
|
||||
provider: result['provider'] as String?,
|
||||
closeDialog: true,
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Close Polling Dialog
|
||||
_onLoginSuccess(jwt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
debugPrint("[Auth] Polling SUCCESS but token missing.");
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Polling error (attempt $attempts): $e");
|
||||
@@ -802,13 +799,36 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Baron SSO",
|
||||
"Baron 통합로그인",
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (_drySendEnabled) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3CD),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFFC107)),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.",
|
||||
style: TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
|
||||
TabBar(
|
||||
|
||||
Reference in New Issue
Block a user