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