forked from baron/baron-sso
기본 발송 중간
This commit is contained in:
@@ -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("코드로 로그인"),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user