1
0
forked from baron/baron-sso

기본 발송 중간

This commit is contained in:
Lectom C Han
2026-01-29 09:28:48 +09:00
parent b88de7ec91
commit 742964cf71
10 changed files with 603 additions and 110 deletions

View File

@@ -39,6 +39,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
int _qrRemainingSeconds = 0;
Timer? _qrCountdownTimer;
int _qrPollIntervalMs = 2000;
final TextEditingController _shortCodePrefixController = TextEditingController();
final TextEditingController _shortCodeDigitsController = TextEditingController();
String? _linkPendingRef;
@override
void initState() {
@@ -52,8 +55,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final uri = Uri.base;
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 shortCode = uri.pathSegments[1];
_verifyShortCode(shortCode);
}
if (loginIdParam != null && codeParam != null) {
_verifyLoginCode(loginIdParam, codeParam);
_verifyLoginCode(loginIdParam, codeParam, pendingRef: pendingRefParam);
} else if (widget.verificationToken != null) {
_verifyToken(widget.verificationToken!);
} else if (uri.queryParameters.containsKey('t')) {
@@ -101,6 +109,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
void _resetLinkLoginState() {
_linkPendingRef = null;
_shortCodePrefixController.clear();
_shortCodeDigitsController.clear();
}
// Helper to decode JWT and get loginId
String _getLoginIdFromJwt(String jwt) {
try {
@@ -306,14 +320,26 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
Future<void> _verifyLoginCode(String loginId, String code) async {
Future<void> _verifyLoginCode(String loginId, String code, {String? pendingRef}) async {
final sanitizedLoginId = loginId.replaceAll(' ', '+');
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
try {
final res = await AuthProxyService.verifyLoginCode(sanitizedLoginId, code);
final res = await AuthProxyService.verifyLoginCode(
sanitizedLoginId,
code,
pendingRef: pendingRef,
);
final jwt = res['sessionJwt'] ?? res['token'];
final status = res['status']?.toString();
debugPrint("[Auth] Code verification successful for loginId: $sanitizedLoginId");
if (jwt == null && status == 'approved') {
if (mounted) {
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
}
return;
}
if (jwt != null && mounted) {
final isJwt = (jwt as String).split('.').length == 3;
if (isJwt) {
@@ -334,6 +360,43 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
Future<void> _verifyShortCode(String shortCode) async {
final sanitized = shortCode.trim().toUpperCase();
if (sanitized.isEmpty) return;
debugPrint("[Auth] Starting short code verification for code: $sanitized");
try {
final res = await AuthProxyService.verifyLoginShortCode(sanitized);
final jwt = res['sessionJwt'] ?? res['token'];
final status = res['status']?.toString();
debugPrint("[Auth] Short code verification successful");
if (jwt == null && status == 'approved') {
if (mounted) {
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
}
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);
}
_onLoginSuccess(jwt, provider: res['provider'] as String?);
}
} catch (e) {
debugPrint("[Auth] Short code verification FAILED. Error: $e");
if (mounted) {
_showError("Verification failed: $e");
}
}
}
@override
void dispose() {
_stopQrPolling();
@@ -341,6 +404,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_linkIdController.dispose();
_passwordLoginIdController.dispose();
_passwordController.dispose();
_shortCodePrefixController.dispose();
_shortCodeDigitsController.dispose();
super.dispose();
}
@@ -434,61 +499,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
debugPrint("[Auth] Link Sent. PendingRef: $pendingRef, Mode: $mode, Provider: $provider");
if (mounted) {
setState(() {
_linkPendingRef = pendingRef?.toString();
});
Navigator.of(context).pop(); // Close Loading
if (mode == 'link' || provider.toLowerCase().contains('ory')) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(isEmail ? "이메일 전송됨" : "SMS 전송됨"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(isEmail
? "입력하신 이메일로 로그인 링크를 보냈습니다."
: "입력하신 번호로 로그인 링크를 보냈습니다."),
const SizedBox(height: 12),
const Text("메일/문자 링크를 열면 이 탭에서 자동으로 로그인됩니다."),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("닫기"),
)
],
),
),
);
return;
}
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(isEmail ? "이메일 전송됨" : "SMS 전송됨"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(isEmail
? "입력하신 이메일로 로그인 링크를 보냈습니다."
: "입력하신 번호로 로그인 링크를 보냈습니다."),
const SizedBox(height: 16),
const LinearProgressIndicator(),
const SizedBox(height: 16),
TextButton(
onPressed: () {
debugPrint("[Auth] Polling canceled by user");
Navigator.of(context).pop();
},
child: const Text("취소"),
)
],
),
),
);
_showInfo(isEmail
? "입력하신 이메일로 로그인 링크를 보냈습니다."
: "입력하신 번호로 로그인 링크를 보냈습니다.");
// 2. Poll Backend manually
final initialInterval = (interval is int && interval > 0)
@@ -499,6 +517,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} catch (e) {
debugPrint("[Auth] Initialization failed: $e");
if (mounted && Navigator.canPop(context)) Navigator.of(context).pop();
if (mounted) {
setState(() {
_linkPendingRef = null;
});
}
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
@@ -584,6 +607,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
void _showInfo(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.green),
);
}
void _logTokenDetails(String jwt) {
try {
final parts = jwt.split('.');
@@ -682,6 +712,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
FilledButton(
onPressed: () {
Navigator.pop(context);
_resetLinkLoginState();
context.push('/signup');
},
child: const Text("회원가입 하기"),
@@ -769,35 +800,90 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
),
// 2. 로그인 링크 전송
// 2. 로그인 링크 전송 -> 전송 후 코드 입력으로 전환
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Column(
children: [
TextField(
controller: _linkIdController,
decoration: const InputDecoration(
labelText: "이메일 또는 휴대폰 번호",
hintText: "",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person_outline),
if (_linkPendingRef == null) ...[
TextField(
controller: _linkIdController,
decoration: const InputDecoration(
labelText: "이메일 또는 휴대폰 번호",
hintText: "",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person_outline),
),
onSubmitted: (_) => _handleLinkLogin(),
),
onSubmitted: (_) => _handleLinkLogin(),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _handleLinkLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(50),
const SizedBox(height: 24),
FilledButton(
onPressed: _handleLinkLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
child: const Text("로그인 링크 전송"),
),
child: const Text("로그인 링크 전송"),
),
const SizedBox(height: 24),
const Text(
"입력하신 정보로 로그인 링크를 전송합니다.",
style: TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
const Text(
"입력하신 정보로 로그인 링크를 전송합니다.",
style: TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
),
],
if (_linkPendingRef != null) ...[
const Text(
"링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.",
style: TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _shortCodePrefixController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
labelText: "AA",
border: OutlineInputBorder(),
),
maxLength: 2,
),
),
const SizedBox(width: 8),
Expanded(
flex: 4,
child: TextField(
controller: _shortCodeDigitsController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "000000",
border: OutlineInputBorder(),
),
maxLength: 6,
),
),
],
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {
final prefix = _shortCodePrefixController.text.trim().toUpperCase();
final digits = _shortCodeDigitsController.text.trim();
if (prefix.length != 2 || digits.length != 6) {
_showError("문자 2개와 숫자 6자리를 입력해 주세요.");
return;
}
_verifyShortCode(prefix + digits);
},
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(45),
),
child: const Text("코드로 로그인"),
),
],
],
),
),