Tunnel component
linuztx committed
May 15, 2025 at 09:11 UTC
482cff765a3b618d55557b7741d747764e2eaebc
1 file changed
+335
webui/js/tunnel.js
new
+335
@@ -0,0 +1,335 @@
1
+// Tunnel settings for the Settings modal
2
+document.addEventListener('alpine:init', () => {
3
+ Alpine.data('tunnelSettings', () => ({
4
+ isLoading: false,
5
+ tunnelLink: '',
6
+ linkGenerated: false,
7
+ loadingText: '',
8
+
9
+ init() {
10
+ this.checkTunnelStatus();
11
+ },
12
+
13
+ async checkTunnelStatus() {
14
+ try {
15
+ const response = await fetch('/tunnel', {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ body: JSON.stringify({ action: 'get' }),
21
+ });
22
+
23
+ const data = await response.json();
24
+
25
+ if (data.success && data.tunnel_url) {
26
+ // Update the stored URL if it's different from what we have
27
+ if (this.tunnelLink !== data.tunnel_url) {
28
+ this.tunnelLink = data.tunnel_url;
29
+ localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
30
+ }
31
+ this.linkGenerated = true;
32
+ } else {
33
+ // Check if we have a stored tunnel URL
34
+ const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url');
35
+
36
+ if (storedTunnelUrl) {
37
+ // Use the stored URL but verify it's still valid
38
+ const verifyResponse = await fetch('/tunnel', {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }),
44
+ });
45
+
46
+ const verifyData = await verifyResponse.json();
47
+
48
+ if (verifyData.success && verifyData.is_valid) {
49
+ this.tunnelLink = storedTunnelUrl;
50
+ this.linkGenerated = true;
51
+ } else {
52
+ // Clear stale URL
53
+ localStorage.removeItem('agent_zero_tunnel_url');
54
+ this.tunnelLink = '';
55
+ this.linkGenerated = false;
56
+ }
57
+ } else {
58
+ // No stored URL, show the generate button
59
+ this.tunnelLink = '';
60
+ this.linkGenerated = false;
61
+ }
62
+ }
63
+ } catch (error) {
64
+ console.error('Error checking tunnel status:', error);
65
+ this.tunnelLink = '';
66
+ this.linkGenerated = false;
67
+ }
68
+ },
69
+
70
+ async refreshLink() {
71
+ // Call generate but with a confirmation first
72
+ if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) {
73
+ this.isLoading = true;
74
+ this.loadingText = 'Refreshing tunnel...';
75
+
76
+ // Change refresh button appearance
77
+ const refreshButton = document.querySelector('.refresh-link-button');
78
+ const originalContent = refreshButton.innerHTML;
79
+ refreshButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
80
+ refreshButton.disabled = true;
81
+ refreshButton.classList.add('refreshing');
82
+
83
+ try {
84
+ // First stop any existing tunnel
85
+ const stopResponse = await fetch('/tunnel', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: JSON.stringify({ action: 'stop' }),
91
+ });
92
+
93
+ // Check if stopping was successful
94
+ const stopData = await stopResponse.json();
95
+ if (!stopData.success) {
96
+ console.warn("Warning: Couldn't stop existing tunnel cleanly");
97
+ // Continue anyway since we want to create a new one
98
+ }
99
+
100
+ // Then generate a new one
101
+ await this.generateLink();
102
+ } catch (error) {
103
+ console.error("Error refreshing tunnel:", error);
104
+ window.toast("Error refreshing tunnel", "error", 3000);
105
+ this.isLoading = false;
106
+ this.loadingText = '';
107
+ } finally {
108
+ // Reset refresh button
109
+ refreshButton.innerHTML = originalContent;
110
+ refreshButton.disabled = false;
111
+ refreshButton.classList.remove('refreshing');
112
+ }
113
+ }
114
+ },
115
+
116
+ async generateLink() {
117
+ // First check if authentication is enabled
118
+ try {
119
+ const authCheckResponse = await fetch('/settings_get');
120
+ const authData = await authCheckResponse.json();
121
+
122
+ // Find the auth_login and auth_password in the settings
123
+ let hasAuth = false;
124
+
125
+ if (authData && authData.settings && authData.settings.sections) {
126
+ for (const section of authData.settings.sections) {
127
+ if (section.fields) {
128
+ const authLoginField = section.fields.find(field => field.id === 'auth_login');
129
+ const authPasswordField = section.fields.find(field => field.id === 'auth_password');
130
+
131
+ if (authLoginField && authPasswordField &&
132
+ authLoginField.value && authPasswordField.value) {
133
+ hasAuth = true;
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ // If no authentication is set, warn the user
141
+ if (!hasAuth) {
142
+ const proceed = confirm(
143
+ "WARNING: No authentication is configured for your Agent Zero instance.\n\n" +
144
+ "Creating a public tunnel without authentication means anyone with the URL " +
145
+ "can access your Agent Zero instance.\n\n" +
146
+ "It is recommended to set up authentication in the Settings > Authentication section " +
147
+ "before creating a public tunnel.\n\n" +
148
+ "Do you want to proceed anyway?"
149
+ );
150
+
151
+ if (!proceed) {
152
+ return; // User cancelled
153
+ }
154
+ }
155
+ } catch (error) {
156
+ console.error("Error checking authentication status:", error);
157
+ // Continue anyway if we can't check auth status
158
+ }
159
+
160
+ this.isLoading = true;
161
+ this.loadingText = 'Creating tunnel...';
162
+
163
+ // Change create button appearance
164
+ const createButton = document.querySelector('.tunnel-actions .btn-ok');
165
+ if (createButton) {
166
+ createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
167
+ createButton.disabled = true;
168
+ createButton.classList.add('creating');
169
+ }
170
+
171
+ try {
172
+ // Call the backend API to create a tunnel
173
+ const response = await fetch('/tunnel', {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ },
178
+ body: JSON.stringify({
179
+ action: 'create',
180
+ port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
181
+ }),
182
+ });
183
+
184
+ const data = await response.json();
185
+
186
+ if (data.success && data.tunnel_url) {
187
+ // Store the tunnel URL in localStorage for persistence
188
+ localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
189
+
190
+ this.tunnelLink = data.tunnel_url;
191
+ this.linkGenerated = true;
192
+
193
+ // Show success message to confirm creation
194
+ window.toast("Tunnel created successfully", "success", 3000);
195
+ } else {
196
+ // The tunnel might still be starting up, check again after a delay
197
+ this.loadingText = 'Tunnel creation taking longer than expected...';
198
+
199
+ // Wait for 5 seconds and check if the tunnel is running
200
+ await new Promise(resolve => setTimeout(resolve, 5000));
201
+
202
+ // Check if tunnel is running now
203
+ try {
204
+ const statusResponse = await fetch('/tunnel', {
205
+ method: 'POST',
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ },
209
+ body: JSON.stringify({ action: 'get' }),
210
+ });
211
+
212
+ const statusData = await statusResponse.json();
213
+
214
+ if (statusData.success && statusData.tunnel_url) {
215
+ // Tunnel is now running, we can update the UI
216
+ localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url);
217
+ this.tunnelLink = statusData.tunnel_url;
218
+ this.linkGenerated = true;
219
+ window.toast("Tunnel created successfully", "success", 3000);
220
+ return;
221
+ }
222
+ } catch (statusError) {
223
+ console.error("Error checking tunnel status:", statusError);
224
+ }
225
+
226
+ // If we get here, the tunnel really failed to start
227
+ const errorMessage = data.message || "Failed to create tunnel. Please try again.";
228
+ window.toast(errorMessage, "error", 5000);
229
+ console.error("Tunnel creation failed:", data);
230
+ }
231
+ } catch (error) {
232
+ window.toast("Error creating tunnel", "error", 5000);
233
+ console.error("Error creating tunnel:", error);
234
+ } finally {
235
+ this.isLoading = false;
236
+ this.loadingText = '';
237
+
238
+ // Reset create button if it's still in the DOM
239
+ const createButton = document.querySelector('.tunnel-actions .btn-ok');
240
+ if (createButton) {
241
+ createButton.innerHTML = '<i class="fas fa-play-circle"></i> Create Tunnel';
242
+ createButton.disabled = false;
243
+ createButton.classList.remove('creating');
244
+ }
245
+ }
246
+ },
247
+
248
+ async stopTunnel() {
249
+ if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) {
250
+ this.isLoading = true;
251
+ this.loadingText = 'Stopping tunnel...';
252
+
253
+
254
+ try {
255
+ // Call the backend to stop the tunnel
256
+ const response = await fetch('/tunnel', {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ body: JSON.stringify({ action: 'stop' }),
262
+ });
263
+
264
+ const data = await response.json();
265
+
266
+ if (data.success) {
267
+ // Clear the stored URL
268
+ localStorage.removeItem('agent_zero_tunnel_url');
269
+
270
+ // Update UI state
271
+ this.tunnelLink = '';
272
+ this.linkGenerated = false;
273
+
274
+ window.toast("Tunnel stopped successfully", "success", 3000);
275
+ } else {
276
+ window.toast("Failed to stop tunnel", "error", 3000);
277
+
278
+ // Reset stop button
279
+ stopButton.innerHTML = originalStopContent;
280
+ stopButton.disabled = false;
281
+ stopButton.classList.remove('stopping');
282
+ }
283
+ } catch (error) {
284
+ window.toast("Error stopping tunnel", "error", 3000);
285
+ console.error("Error stopping tunnel:", error);
286
+
287
+ // Reset stop button
288
+ stopButton.innerHTML = originalStopContent;
289
+ stopButton.disabled = false;
290
+ stopButton.classList.remove('stopping');
291
+ } finally {
292
+ this.isLoading = false;
293
+ this.loadingText = '';
294
+ }
295
+ }
296
+ },
297
+
298
+ copyToClipboard() {
299
+ if (!this.tunnelLink) return;
300
+
301
+ const copyButton = document.querySelector('.copy-link-button');
302
+ const originalContent = copyButton.innerHTML;
303
+
304
+ navigator.clipboard.writeText(this.tunnelLink)
305
+ .then(() => {
306
+ // Update button to show success state
307
+ copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
308
+ copyButton.classList.add('copy-success');
309
+
310
+ // Show toast notification
311
+ window.toast("Tunnel URL copied to clipboard!", "success", 3000);
312
+
313
+ // Reset button after 2 seconds
314
+ setTimeout(() => {
315
+ copyButton.innerHTML = originalContent;
316
+ copyButton.classList.remove('copy-success');
317
+ }, 2000);
318
+ })
319
+ .catch(err => {
320
+ console.error('Failed to copy URL: ', err);
321
+ window.toast("Failed to copy tunnel URL", "error", 3000);
322
+
323
+ // Show error state
324
+ copyButton.innerHTML = '<i class="fas fa-times"></i> Failed';
325
+ copyButton.classList.add('copy-error');
326
+
327
+ // Reset button after 2 seconds
328
+ setTimeout(() => {
329
+ copyButton.innerHTML = originalContent;
330
+ copyButton.classList.remove('copy-error');
331
+ }, 2000);
332
+ });
333
+ }
334
+ }));
335
+});
\ No newline at end of file