mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 18:28:25 +08:00
feat: removed heartbeat monitoring
This commit is contained in:
@@ -10,9 +10,7 @@ class OAuthCallbackServer {
|
|||||||
String? _path;
|
String? _path;
|
||||||
final Completer<String> _completer = Completer<String>();
|
final Completer<String> _completer = Completer<String>();
|
||||||
Timer? _timeoutTimer;
|
Timer? _timeoutTimer;
|
||||||
Timer? _heartbeatTimer;
|
bool _isCancelled = false;
|
||||||
DateTime? _lastHeartbeat;
|
|
||||||
bool _browserTabActive = false;
|
|
||||||
|
|
||||||
/// Starts the HTTP server and returns the callback URL.
|
/// Starts the HTTP server and returns the callback URL.
|
||||||
///
|
///
|
||||||
@@ -53,17 +51,22 @@ class OAuthCallbackServer {
|
|||||||
///
|
///
|
||||||
/// [timeout] - Optional timeout duration (defaults to 3 minutes)
|
/// [timeout] - Optional timeout duration (defaults to 3 minutes)
|
||||||
/// Throws [TimeoutException] if no callback is received within the timeout period.
|
/// Throws [TimeoutException] if no callback is received within the timeout period.
|
||||||
/// Throws [Exception] if the browser tab is closed without completing authorization.
|
/// Throws [Exception] if the OAuth flow was manually cancelled.
|
||||||
Future<String> waitForCallback({
|
Future<String> waitForCallback({
|
||||||
Duration timeout = const Duration(minutes: 3),
|
Duration timeout = const Duration(minutes: 3),
|
||||||
}) async {
|
}) async {
|
||||||
|
// Check if already cancelled before starting
|
||||||
|
if (_isCancelled) {
|
||||||
|
throw Exception('OAuth flow was cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
// Set up timeout timer
|
// Set up timeout timer
|
||||||
_timeoutTimer = Timer(timeout, () {
|
_timeoutTimer = Timer(timeout, () {
|
||||||
if (!_completer.isCompleted) {
|
if (!_completer.isCompleted && !_isCancelled) {
|
||||||
_completer.completeError(
|
_completer.completeError(
|
||||||
TimeoutException(
|
TimeoutException(
|
||||||
'OAuth callback timeout: No response received within ${timeout.inMinutes} minutes. '
|
'OAuth callback timeout: No response received within ${timeout.inMinutes} minutes. '
|
||||||
'The user may have closed the browser tab without completing authorization.',
|
'You can manually cancel this operation or wait for completion.',
|
||||||
timeout,
|
timeout,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -75,15 +78,11 @@ class OAuthCallbackServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up heartbeat monitoring to detect if browser tab is closed
|
|
||||||
_startHeartbeatMonitoring();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await _completer.future;
|
return await _completer.future;
|
||||||
} finally {
|
} finally {
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = null;
|
_timeoutTimer = null;
|
||||||
_stopHeartbeatMonitoring();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +95,6 @@ class OAuthCallbackServer {
|
|||||||
|
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = null;
|
_timeoutTimer = null;
|
||||||
_stopHeartbeatMonitoring();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _server?.close();
|
await _server?.close();
|
||||||
@@ -111,9 +109,9 @@ class OAuthCallbackServer {
|
|||||||
/// Cancels the waiting callback operation.
|
/// Cancels the waiting callback operation.
|
||||||
/// This is useful when the user wants to cancel the OAuth flow manually.
|
/// This is useful when the user wants to cancel the OAuth flow manually.
|
||||||
void cancel([String reason = 'OAuth flow cancelled by user']) {
|
void cancel([String reason = 'OAuth flow cancelled by user']) {
|
||||||
|
_isCancelled = true;
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = null;
|
_timeoutTimer = null;
|
||||||
_stopHeartbeatMonitoring();
|
|
||||||
|
|
||||||
if (!_completer.isCompleted) {
|
if (!_completer.isCompleted) {
|
||||||
_completer.completeError(Exception('OAuth callback cancelled: $reason'));
|
_completer.completeError(Exception('OAuth callback cancelled: $reason'));
|
||||||
@@ -123,48 +121,8 @@ class OAuthCallbackServer {
|
|||||||
_stopServerOnError(reason);
|
_stopServerOnError(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts heartbeat monitoring to detect browser tab closure
|
/// Checks if the OAuth flow was cancelled
|
||||||
void _startHeartbeatMonitoring() {
|
bool get isCancelled => _isCancelled;
|
||||||
_lastHeartbeat = DateTime.now();
|
|
||||||
_browserTabActive = true;
|
|
||||||
|
|
||||||
// Check for heartbeat every 5 seconds
|
|
||||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
// If no heartbeat received for 5 seconds and we had an active tab before
|
|
||||||
if (_browserTabActive &&
|
|
||||||
_lastHeartbeat != null &&
|
|
||||||
now.difference(_lastHeartbeat!).inSeconds > 5) {
|
|
||||||
log(
|
|
||||||
'Browser tab appears to be closed (no heartbeat for ${now.difference(_lastHeartbeat!).inSeconds}s)',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!_completer.isCompleted) {
|
|
||||||
_completer.completeError(
|
|
||||||
Exception(
|
|
||||||
'OAuth authorization cancelled: Browser tab was closed without completing the authorization process. '
|
|
||||||
'Please try again and complete the authorization in your browser.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
timer.cancel();
|
|
||||||
|
|
||||||
// Automatically stop the server when browser tab is closed
|
|
||||||
_stopServerOnError(
|
|
||||||
'Browser tab closed without completing authorization',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops heartbeat monitoring
|
|
||||||
void _stopHeartbeatMonitoring() {
|
|
||||||
_heartbeatTimer?.cancel();
|
|
||||||
_heartbeatTimer = null;
|
|
||||||
_lastHeartbeat = null;
|
|
||||||
_browserTabActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the server immediately due to an error condition
|
/// Stops the server immediately due to an error condition
|
||||||
/// This is used for automatic cleanup when errors occur
|
/// This is used for automatic cleanup when errors occur
|
||||||
@@ -179,7 +137,6 @@ class OAuthCallbackServer {
|
|||||||
// Cancel any active timers
|
// Cancel any active timers
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = null;
|
_timeoutTimer = null;
|
||||||
_stopHeartbeatMonitoring();
|
|
||||||
|
|
||||||
// Close the server without waiting
|
// Close the server without waiting
|
||||||
_server
|
_server
|
||||||
@@ -198,20 +155,6 @@ class OAuthCallbackServer {
|
|||||||
log('OAuth request received: ${request.uri}');
|
log('OAuth request received: ${request.uri}');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle heartbeat requests
|
|
||||||
if (request.uri.path == '/heartbeat') {
|
|
||||||
_lastHeartbeat = DateTime.now();
|
|
||||||
_browserTabActive = true;
|
|
||||||
request.response
|
|
||||||
..statusCode = HttpStatus.ok
|
|
||||||
..headers.add('Access-Control-Allow-Origin', '*')
|
|
||||||
..headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
||||||
..headers.add('Access-Control-Allow-Headers', 'Content-Type')
|
|
||||||
..write('ok')
|
|
||||||
..close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle OPTIONS preflight requests for CORS
|
// Handle OPTIONS preflight requests for CORS
|
||||||
if (request.method == 'OPTIONS') {
|
if (request.method == 'OPTIONS') {
|
||||||
request.response
|
request.response
|
||||||
@@ -335,39 +278,15 @@ class OAuthCallbackServer {
|
|||||||
let seconds = 5;
|
let seconds = 5;
|
||||||
const timer = document.getElementById('timer');
|
const timer = document.getElementById('timer');
|
||||||
|
|
||||||
// Send heartbeat to let server know we're still active
|
|
||||||
const sendHeartbeat = () => {
|
|
||||||
fetch('/heartbeat', { method: 'GET', mode: 'no-cors' }).catch(() => {
|
|
||||||
// Ignore fetch errors as we're just sending a signal
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send initial heartbeat and set up periodic heartbeats
|
|
||||||
sendHeartbeat();
|
|
||||||
const heartbeatInterval = setInterval(sendHeartbeat, 2000);
|
|
||||||
|
|
||||||
// Countdown timer
|
// Countdown timer
|
||||||
const countdown = setInterval(() => {
|
const countdown = setInterval(() => {
|
||||||
seconds--;
|
seconds--;
|
||||||
timer.textContent = seconds;
|
timer.textContent = seconds;
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
clearInterval(countdown);
|
clearInterval(countdown);
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Send notification when window is about to close
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
// Try to send final heartbeat to indicate closure
|
|
||||||
navigator.sendBeacon('/heartbeat', 'closing');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle when user manually closes the window
|
|
||||||
window.addEventListener('unload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -408,37 +327,15 @@ class OAuthCallbackServer {
|
|||||||
let seconds = 10;
|
let seconds = 10;
|
||||||
const timer = document.getElementById('timer');
|
const timer = document.getElementById('timer');
|
||||||
|
|
||||||
// Send heartbeat to let server know we're still active
|
|
||||||
const sendHeartbeat = () => {
|
|
||||||
fetch('/heartbeat', { method: 'GET', mode: 'no-cors' }).catch(() => {
|
|
||||||
// Ignore fetch errors
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send initial heartbeat and set up periodic heartbeats
|
|
||||||
sendHeartbeat();
|
|
||||||
const heartbeatInterval = setInterval(sendHeartbeat, 2000);
|
|
||||||
|
|
||||||
// Countdown timer
|
// Countdown timer
|
||||||
const countdown = setInterval(() => {
|
const countdown = setInterval(() => {
|
||||||
seconds--;
|
seconds--;
|
||||||
timer.textContent = seconds;
|
timer.textContent = seconds;
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
clearInterval(countdown);
|
clearInterval(countdown);
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Handle window closing events
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
navigator.sendBeacon('/heartbeat', 'closing');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -469,28 +366,6 @@ class OAuthCallbackServer {
|
|||||||
Please return to API Dash to complete the OAuth flow.
|
Please return to API Dash to complete the OAuth flow.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
// Send heartbeat to let server know we're still active even on info page
|
|
||||||
const sendHeartbeat = () => {
|
|
||||||
fetch('/heartbeat', { method: 'GET', mode: 'no-cors' }).catch(() => {
|
|
||||||
// Ignore fetch errors
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send initial heartbeat and set up periodic heartbeats
|
|
||||||
sendHeartbeat();
|
|
||||||
const heartbeatInterval = setInterval(sendHeartbeat, 2000);
|
|
||||||
|
|
||||||
// Handle window closing events
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
navigator.sendBeacon('/heartbeat', 'closing');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
|
||||||
clearInterval(heartbeatInterval);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
''';
|
''';
|
||||||
|
|||||||
Reference in New Issue
Block a user