forked from baron/baron-sso
163 lines
5.2 KiB
Dart
163 lines
5.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:descope/descope.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../../../core/services/auth_proxy_service.dart';
|
|
import '../../../../core/services/auth_token_store.dart';
|
|
|
|
class ApproveQrScreen extends StatefulWidget {
|
|
final String? pendingRef;
|
|
const ApproveQrScreen({super.key, this.pendingRef});
|
|
|
|
@override
|
|
State<ApproveQrScreen> createState() => _ApproveQrScreenState();
|
|
}
|
|
|
|
class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
|
bool _isLoading = false;
|
|
String? _message;
|
|
bool _success = false;
|
|
bool _isCheckingSession = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_bootstrapCookieSession();
|
|
}
|
|
|
|
Future<bool> _bootstrapCookieSession() async {
|
|
if (AuthTokenStore.usesCookie()) {
|
|
return true;
|
|
}
|
|
if (_isCheckingSession) {
|
|
return false;
|
|
}
|
|
setState(() => _isCheckingSession = true);
|
|
try {
|
|
await AuthProxyService.checkCookieSession();
|
|
AuthTokenStore.setCookieMode(provider: 'ory');
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => _isCheckingSession = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _handleApprove() async {
|
|
if (widget.pendingRef == null) return;
|
|
|
|
final storedToken = AuthTokenStore.getToken();
|
|
final session = Descope.sessionManager.session;
|
|
final usesCookie = AuthTokenStore.usesCookie();
|
|
var hasCookie = usesCookie;
|
|
if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) {
|
|
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
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
_message = null;
|
|
});
|
|
// jwt 유효성 확인
|
|
try {
|
|
final token = storedToken ?? session?.sessionToken.jwt ?? '';
|
|
await AuthProxyService.approveQrLogin(
|
|
widget.pendingRef!,
|
|
token: token,
|
|
withCredentials: hasCookie,
|
|
);
|
|
setState(() {
|
|
_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('/');
|
|
});
|
|
} catch (e) {
|
|
setState(() => _message = "Error: $e");
|
|
} finally {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasStoredToken = AuthTokenStore.getToken() != null;
|
|
final hasDescopeSession = Descope.sessionManager.session?.refreshToken.isExpired == false;
|
|
final usesCookie = AuthTokenStore.usesCookie();
|
|
final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie || _isCheckingSession;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text("QR Login Approval")),
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.phonelink_lock, size: 80, color: Colors.blue),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
"Web Login Request",
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
"A computer is trying to log in using this QR code.",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.grey.shade600),
|
|
),
|
|
const SizedBox(height: 40),
|
|
|
|
if (_message != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: Text(
|
|
_message!,
|
|
style: TextStyle(color: _success ? Colors.green : Colors.red, fontWeight: FontWeight.bold),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
|
|
if (!_success)
|
|
FilledButton.icon(
|
|
onPressed: _isLoading || !isLoggedIn ? null : _handleApprove,
|
|
icon: const Icon(Icons.check_circle),
|
|
label: const Text("Approve Login"),
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(60),
|
|
backgroundColor: Colors.blue,
|
|
),
|
|
),
|
|
|
|
if (!isLoggedIn && !_success)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16),
|
|
child: TextButton(
|
|
onPressed: () => context.go('/signin'),
|
|
child: const Text("Login on this device first"),
|
|
),
|
|
),
|
|
|
|
if (_success)
|
|
FilledButton(
|
|
onPressed: () => context.go('/'),
|
|
child: const Text("Go to My Dashboard"),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|