forked from baron/baron-sso
대시보드 qr 페이지
This commit is contained in:
@@ -40,6 +40,11 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
_success = true;
|
||||
_message = "Login Approved! Your browser should now be logged in.";
|
||||
});
|
||||
|
||||
// Automatically go to dashboard after a short delay
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (mounted) context.go('/dashboard');
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _message = "Error: $e");
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '../../../core/services/audit_service.dart';
|
||||
import '../../../core/services/web_auth_integration.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/notifiers/auth_notifier.dart';
|
||||
|
||||
class LoginScreen extends ConsumerStatefulWidget {
|
||||
final String? verificationToken;
|
||||
@@ -122,7 +123,33 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (res['status'] == 'ok' && res['sessionJwt'] != null) {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
_onLoginSuccess(res['sessionJwt']);
|
||||
|
||||
final jwt = res['sessionJwt'];
|
||||
// Create Dummy User & Session for Descope SDK
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', // userId
|
||||
[], // loginIds
|
||||
0, // createdAt
|
||||
'User', // 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(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
|
||||
_onLoginSuccess(jwt);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[QR] Polling error: $e");
|
||||
@@ -148,11 +175,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint("[Auth] Starting verification for token: $token");
|
||||
try {
|
||||
// Use Backend to verify the token (Backend-Driven Flow)
|
||||
await AuthProxyService.verifyMagicLink(token);
|
||||
final res = await AuthProxyService.verifyMagicLink(token);
|
||||
final jwt = res['token'];
|
||||
debugPrint("[Auth] Verification successful for token: $token");
|
||||
|
||||
if (mounted) {
|
||||
_showSuccessDialog();
|
||||
if (jwt != null && mounted) {
|
||||
// Create Dummy User & Session for Descope SDK to log in this tab
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, 'User', null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
|
||||
// Notify and Go to Dashboard
|
||||
_onLoginSuccess(jwt);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
||||
@@ -405,15 +441,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
details: "User logged in via Baron SSO",
|
||||
);
|
||||
|
||||
if (WebAuthIntegration.isPopup()) {
|
||||
WebAuthIntegration.sendLoginSuccess(token);
|
||||
_showError("Login Successful! You can close this window.");
|
||||
} else if (_redirectUrl != null && _redirectUrl!.isNotEmpty) {
|
||||
// 1. Handle Redirect Flow (Redirect to another app)
|
||||
if (_redirectUrl != null && _redirectUrl!.isNotEmpty) {
|
||||
final target = "$_redirectUrl?token=$token";
|
||||
launchUrlString(target, webOnlyWindowName: '_self');
|
||||
} else {
|
||||
// Standalone mode: Go to dashboard to act as an auth platform
|
||||
if (mounted) context.go('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Handle Popup Flow (Send message to opener)
|
||||
if (WebAuthIntegration.isPopup()) {
|
||||
WebAuthIntegration.sendLoginSuccess(token);
|
||||
// If this window was truly a popup for another app, it should close now.
|
||||
// If it's still here, we allow it to fall through to the dashboard.
|
||||
}
|
||||
|
||||
// 3. Standalone mode: Go to dashboard
|
||||
// We call notify() to update the router's state, and go() to ensure navigation.
|
||||
AuthNotifier.instance.notify();
|
||||
if (mounted) {
|
||||
context.go('/dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class LoginSuccessScreen extends StatelessWidget {
|
||||
const LoginSuccessScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, size: 80, color: Colors.green),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"로그인 완료",
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"성공적으로 로그인되었습니다.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// 이 버튼이 QR 카메라를 켜는 버튼입니다.
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
context.push('/qr-scan');
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, size: 28),
|
||||
label: const Text("QR 인증 (카메라 켜기)"),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/dashboard');
|
||||
},
|
||||
child: const Text("나중에 하기 (대시보드로 이동)", style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
122
frontend/lib/features/auth/presentation/qr_scan_screen.dart
Normal file
122
frontend/lib/features/auth/presentation/qr_scan_screen.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
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';
|
||||
|
||||
class QRScanScreen extends StatefulWidget {
|
||||
const QRScanScreen({super.key});
|
||||
|
||||
@override
|
||||
State<QRScanScreen> createState() => _QRScanScreenState();
|
||||
}
|
||||
|
||||
class _QRScanScreenState extends State<QRScanScreen> {
|
||||
final _log = Logger('QRScanScreen');
|
||||
final MobileScannerController controller = MobileScannerController(
|
||||
detectionSpeed: DetectionSpeed.noDuplicates,
|
||||
);
|
||||
bool _isScanned = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onDetect(BarcodeCapture capture) async {
|
||||
if (_isScanned) return;
|
||||
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
for (final barcode in barcodes) {
|
||||
if (barcode.rawValue != null) {
|
||||
_isScanned = true;
|
||||
String qrData = barcode.rawValue!;
|
||||
String pendingRef = qrData;
|
||||
|
||||
// URL 형식이라면 'ref' 파라미터 추출 시도
|
||||
if (qrData.startsWith('http')) {
|
||||
try {
|
||||
final uri = Uri.parse(qrData);
|
||||
if (uri.queryParameters.containsKey('ref')) {
|
||||
pendingRef = uri.queryParameters['ref']!;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning('Failed to parse QR URL: $qrData', e);
|
||||
}
|
||||
}
|
||||
|
||||
_log.info('QR Code detected raw: $qrData, ref: $pendingRef');
|
||||
|
||||
final sessionToken = Descope.sessionManager.session?.sessionToken.jwt;
|
||||
if (sessionToken == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('로그인이 필요합니다.'), backgroundColor: Colors.red),
|
||||
);
|
||||
context.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call backend API to approve login with clean ref
|
||||
await AuthProxyService.approveQrLogin(pendingRef, sessionToken);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('로그인 승인 완료!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
// Wait a bit and go back
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted) context.pop();
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("QR Approval Failed", e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('승인 실패: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
// Allow rescanning after a delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
_isScanned = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR Code'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: MobileScanner(
|
||||
controller: controller,
|
||||
onDetect: _onDetect,
|
||||
errorBuilder: (context, error, child) {
|
||||
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}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user