forked from baron/baron-sso
userfront gateway 분리.
This commit is contained in:
@@ -17,11 +17,12 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
String? _message;
|
||||
bool _success = false;
|
||||
bool _isCheckingSession = false;
|
||||
bool _redirectingToLogin = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bootstrapCookieSession();
|
||||
_bootstrapCookieSession().then((_) => _redirectIfNotLoggedIn());
|
||||
}
|
||||
|
||||
Future<bool> _bootstrapCookieSession() async {
|
||||
@@ -45,6 +46,21 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!isLoggedIn) {
|
||||
_redirectingToLogin = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
context.go('/signin?notice=qr_login_required');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleApprove() async {
|
||||
if (widget.pendingRef == null) return;
|
||||
|
||||
@@ -56,8 +72,9 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
hasCookie = await _bootstrapCookieSession();
|
||||
}
|
||||
if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) {
|
||||
setState(() => _message = "Please log in on your phone first.");
|
||||
context.go('/signin'); // Redirect to login
|
||||
if (mounted) {
|
||||
context.go('/signin?notice=qr_login_required');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,6 +113,10 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
final usesCookie = AuthTokenStore.usesCookie();
|
||||
final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie || _isCheckingSession;
|
||||
|
||||
if (!isLoggedIn && !_redirectingToLogin) {
|
||||
_redirectIfNotLoggedIn();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("QR Login Approval")),
|
||||
body: Center(
|
||||
|
||||
@@ -48,6 +48,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool _verificationOnly = false;
|
||||
bool _verificationApproved = false;
|
||||
String _verificationMessage = '';
|
||||
String _verificationTitle = '승인 완료';
|
||||
String _verificationPageTitle = '로그인 승인';
|
||||
String _verificationActionLabel = '확인';
|
||||
String _verificationActionPath = '/';
|
||||
Timer? _verificationRedirectTimer;
|
||||
bool _noticeHandled = false;
|
||||
bool _drySendEnabled = false;
|
||||
|
||||
@override
|
||||
@@ -69,6 +75,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final hasVerificationToken = widget.verificationToken != null || hasTokenParam;
|
||||
final hasLoginCode = loginIdParam != null && codeParam != null;
|
||||
_verificationOnly = hasVerificationToken || hasLoginCode || hasShortCodePath;
|
||||
final notice = uri.queryParameters['notice'];
|
||||
|
||||
if (hasShortCodePath) {
|
||||
final shortCode = uri.pathSegments[1];
|
||||
@@ -80,6 +87,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_verifyToken(widget.verificationToken ?? uri.queryParameters['t']!);
|
||||
}
|
||||
|
||||
if (!_noticeHandled && notice == 'qr_login_required') {
|
||||
_noticeHandled = true;
|
||||
_showInfo('로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다');
|
||||
}
|
||||
|
||||
if (!_verificationOnly) {
|
||||
_tryCookieSession();
|
||||
}
|
||||
@@ -360,12 +372,31 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return AuthTokenStore.usesCookie();
|
||||
}
|
||||
|
||||
void _markVerificationApproved(String message) {
|
||||
void _markVerificationApproved(
|
||||
String message, {
|
||||
String title = '승인 완료',
|
||||
String pageTitle = '로그인 승인',
|
||||
String actionLabel = '확인',
|
||||
String actionPath = '/',
|
||||
bool autoRedirect = false,
|
||||
Duration redirectDelay = const Duration(seconds: 2),
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
_verificationTitle = title;
|
||||
_verificationPageTitle = pageTitle;
|
||||
_verificationActionLabel = actionLabel;
|
||||
_verificationActionPath = actionPath;
|
||||
});
|
||||
_verificationRedirectTimer?.cancel();
|
||||
if (autoRedirect) {
|
||||
_verificationRedirectTimer = Timer(redirectDelay, () {
|
||||
if (!mounted) return;
|
||||
context.go(actionPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVerificationResultView() {
|
||||
@@ -377,9 +408,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, color: Colors.green, size: 72),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'승인 완료',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
|
||||
Text(
|
||||
_verificationTitle,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
@@ -389,8 +420,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('확인'),
|
||||
onPressed: () => context.go(_verificationActionPath),
|
||||
child: Text(_verificationActionLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -409,11 +440,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final hasLocalSession = _hasLocalSession();
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
);
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: provider);
|
||||
_markVerificationApproved(
|
||||
"링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.",
|
||||
title: '링크 로그인 완료',
|
||||
pageTitle: '링크 로그인',
|
||||
actionLabel: '로그인 화면으로 이동',
|
||||
actionPath: '/signin',
|
||||
autoRedirect: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -451,7 +491,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
);
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
@@ -489,7 +531,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
);
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
@@ -510,6 +554,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
@override
|
||||
void dispose() {
|
||||
_stopQrPolling();
|
||||
_verificationRedirectTimer?.cancel();
|
||||
_tabController.dispose();
|
||||
_linkIdController.dispose();
|
||||
_passwordLoginIdController.dispose();
|
||||
@@ -851,7 +896,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (_verificationOnly && _verificationApproved) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('로그인 승인'),
|
||||
title: Text(_verificationPageTitle),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
|
||||
@@ -21,6 +21,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
bool _isScanned = false;
|
||||
bool _isCheckingSession = false;
|
||||
bool _isProcessing = false;
|
||||
bool _isRequestingCamera = false;
|
||||
bool? _isSuccess;
|
||||
String? _resultMessage;
|
||||
|
||||
@@ -100,10 +101,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
}
|
||||
if (sessionToken == null && !usesCookie) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('로그인이 필요합니다.'), backgroundColor: Colors.red),
|
||||
);
|
||||
context.pop();
|
||||
context.go('/signin?notice=qr_login_required');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -148,6 +146,28 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
controller.start();
|
||||
}
|
||||
|
||||
Future<void> _requestCameraPermission() async {
|
||||
if (_isRequestingCamera) return;
|
||||
setState(() => _isRequestingCamera = true);
|
||||
try {
|
||||
await controller.start();
|
||||
} catch (e) {
|
||||
_log.warning('Camera permission request failed: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isRequestingCamera = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildResultView() {
|
||||
final success = _isSuccess == true;
|
||||
final icon = success ? Icons.check_circle_outline : Icons.error_outline;
|
||||
@@ -207,13 +227,30 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
controller: controller,
|
||||
onDetect: _onDetect,
|
||||
errorBuilder: (context, error) {
|
||||
final isPermissionDenied = error.errorCode ==
|
||||
MobileScannerErrorCode.permissionDenied;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red, size: 50),
|
||||
const SizedBox(height: 10),
|
||||
Text('Camera Error: ${error.errorCode}'),
|
||||
Text(
|
||||
isPermissionDenied
|
||||
? '카메라 권한이 필요합니다.'
|
||||
: '카메라 오류: ${error.errorCode}',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: _isRequestingCamera
|
||||
? null
|
||||
: _requestCameraPermission,
|
||||
child: Text(
|
||||
_isRequestingCamera
|
||||
? '요청 중...'
|
||||
: '카메라 권한 요청하기',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user