mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Added outline server managment page template
This commit is contained in:
176
vpn/templates/admin/vpn/outlineserver/add_form.html.backup
Normal file
176
vpn/templates/admin/vpn/outlineserver/add_form.html.backup
Normal file
@@ -0,0 +1,176 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
{% load i18n admin_urls static %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add JSON Import tab
|
||||
const tabList = document.getElementById('jazzy-tabs');
|
||||
const tabContent = document.querySelector('.tab-content');
|
||||
|
||||
if (tabList && tabContent) {
|
||||
// Add new tab
|
||||
const newTab = document.createElement('li');
|
||||
newTab.className = 'nav-item';
|
||||
newTab.innerHTML = `
|
||||
<a class="nav-link" data-toggle="pill" role="tab" aria-controls="json-import-tab" aria-selected="false" href="#json-import-tab">
|
||||
📥 JSON Import
|
||||
</a>
|
||||
`;
|
||||
tabList.insertBefore(newTab, tabList.firstChild);
|
||||
|
||||
// Add tab content
|
||||
const newTabContent = document.createElement('div');
|
||||
newTabContent.id = 'json-import-tab';
|
||||
newTabContent.className = 'tab-pane fade';
|
||||
newTabContent.setAttribute('role', 'tabpanel');
|
||||
newTabContent.setAttribute('aria-labelledby', 'json-import-tab');
|
||||
newTabContent.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="p-5 card-body">
|
||||
<h4 style="color: #007cba; margin-bottom: 1rem;">📥 Quick Import from JSON</h4>
|
||||
<p style="font-size: 0.875rem; color: #6c757d; margin-bottom: 1rem;">
|
||||
Paste the JSON configuration from your Outline server setup to automatically fill the fields:
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="import-json-config">JSON Configuration:</label>
|
||||
<textarea id="import-json-config" class="form-control" rows="8"
|
||||
placeholder='{
|
||||
"apiUrl": "https://your-server:port/path",
|
||||
"certSha256": "your-certificate-hash",
|
||||
"serverName": "My Outline Server",
|
||||
"clientHostname": "your-server.com",
|
||||
"clientPort": 1257,
|
||||
"comment": "Server description"
|
||||
}' style="font-family: 'Courier New', monospace; font-size: 0.875rem;"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" id="import-json-btn" class="btn btn-primary">
|
||||
Import Configuration
|
||||
</button>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: #e7f3ff; border-left: 4px solid #007cba; border-radius: 4px;">
|
||||
<strong>Required fields:</strong>
|
||||
<ul style="margin: 0.5rem 0; padding-left: 20px;">
|
||||
<li><code>apiUrl</code> - Server management URL</li>
|
||||
<li><code>certSha256</code> - Certificate fingerprint</li>
|
||||
</ul>
|
||||
<strong>Optional fields:</strong>
|
||||
<ul style="margin: 0.5rem 0; padding-left: 20px;">
|
||||
<li><code>serverName</code> - Display name</li>
|
||||
<li><code>clientHostname</code> - Client hostname</li>
|
||||
<li><code>clientPort</code> - Client port</li>
|
||||
<li><code>comment</code> - Description</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
tabContent.insertBefore(newTabContent, tabContent.firstChild);
|
||||
|
||||
// Make first tab (JSON Import) active
|
||||
document.querySelector('#jazzy-tabs .nav-link').classList.remove('active');
|
||||
newTab.querySelector('.nav-link').classList.add('active');
|
||||
document.querySelector('.tab-pane.active').classList.remove('active', 'show');
|
||||
newTabContent.classList.add('active', 'show');
|
||||
}
|
||||
|
||||
// Import functionality
|
||||
function tryAutoFillFromJson() {
|
||||
const importJsonTextarea = document.getElementById('import-json-config');
|
||||
|
||||
try {
|
||||
const jsonText = importJsonTextarea.value.trim();
|
||||
if (!jsonText) {
|
||||
alert('Please enter JSON configuration');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(jsonText);
|
||||
|
||||
// Validate required fields
|
||||
if (!config.apiUrl || !config.certSha256) {
|
||||
alert('Invalid JSON format. Required fields: apiUrl, certSha256');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse apiUrl to extract components
|
||||
const url = new URL(config.apiUrl);
|
||||
|
||||
// Fill form fields
|
||||
const adminUrlField = document.getElementById('id_admin_url');
|
||||
const adminCertField = document.getElementById('id_admin_access_cert');
|
||||
const clientHostnameField = document.getElementById('id_client_hostname');
|
||||
const clientPortField = document.getElementById('id_client_port');
|
||||
const nameField = document.getElementById('id_name');
|
||||
const commentField = document.getElementById('id_comment');
|
||||
|
||||
if (adminUrlField) adminUrlField.value = config.apiUrl;
|
||||
if (adminCertField) adminCertField.value = config.certSha256;
|
||||
|
||||
// Use provided hostname or extract from URL
|
||||
const hostname = config.clientHostname || config.hostnameForAccessKeys || url.hostname;
|
||||
if (clientHostnameField) clientHostnameField.value = hostname;
|
||||
|
||||
// Use provided port or extract from various sources
|
||||
const clientPort = config.clientPort || config.portForNewAccessKeys || url.port || '1257';
|
||||
if (clientPortField) clientPortField.value = clientPort;
|
||||
|
||||
// Generate server name if not provided and field is empty
|
||||
if (nameField && !nameField.value) {
|
||||
const serverName = config.serverName || config.name || `Outline-${hostname}`;
|
||||
nameField.value = serverName;
|
||||
}
|
||||
|
||||
// Fill comment if provided and field exists
|
||||
if (commentField && config.comment) {
|
||||
commentField.value = config.comment;
|
||||
}
|
||||
|
||||
// Clear the JSON input
|
||||
importJsonTextarea.value = '';
|
||||
|
||||
// Show success message
|
||||
alert('✅ Configuration imported successfully! Review the fields and save.');
|
||||
|
||||
// Switch to Server Configuration tab
|
||||
const serverConfigTab = document.querySelector('a[href="#server-configuration-tab"]');
|
||||
if (serverConfigTab) {
|
||||
serverConfigTab.click();
|
||||
}
|
||||
|
||||
// Focus on name field
|
||||
if (nameField) {
|
||||
setTimeout(() => {
|
||||
nameField.focus();
|
||||
nameField.select();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert(`Invalid JSON format: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a bit for DOM to be ready, then add event listeners
|
||||
setTimeout(() => {
|
||||
const importBtn = document.getElementById('import-json-btn');
|
||||
const importTextarea = document.getElementById('import-json-config');
|
||||
|
||||
if (importBtn) {
|
||||
importBtn.addEventListener('click', tryAutoFillFromJson);
|
||||
}
|
||||
|
||||
if (importTextarea) {
|
||||
importTextarea.addEventListener('paste', function(e) {
|
||||
setTimeout(() => {
|
||||
tryAutoFillFromJson();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
23
vpn/templates/admin/vpn/outlineserver/change_form.html
Normal file
23
vpn/templates/admin/vpn/outlineserver/change_form.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content_title %}
|
||||
<h1 class="h4 m-0 pr-3 mr-3 border-right">
|
||||
{% if original %}
|
||||
🔵 Outline Server: {{ original.name }}
|
||||
{% else %}
|
||||
🔵 Add Outline Server
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_change_form_document_ready %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
// All JavaScript functionality is now handled by generate_link.js
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block field_sets %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
@@ -11,272 +11,81 @@
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if original and servers_data %}
|
||||
<!-- User Access Management Panel -->
|
||||
<div class="user-access-management" style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 24px 0;">
|
||||
<h3 style="margin: 0 0 20px 0; color: #1f2937; font-size: 18px;">🔗 User Access Management</h3>
|
||||
|
||||
<!-- User Portal Links -->
|
||||
<div style="background: #eff6ff; border: 1px solid #dbeafe; border-radius: 6px; padding: 16px; margin-bottom: 20px;">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1e40af; font-size: 14px;">📱 User Portal Access</h4>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<a href="{{ external_address }}/u/{{ original.hash }}" target="_blank"
|
||||
style="background: #4ade80; color: #000; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-weight: bold;">
|
||||
🌐 Open User Portal
|
||||
</a>
|
||||
<a href="{{ external_address }}/stat/{{ original.hash }}" target="_blank"
|
||||
style="background: #3b82f6; color: #fff; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-weight: bold;">
|
||||
📄 JSON API
|
||||
</a>
|
||||
<span style="background: #f3f4f6; padding: 8px 12px; border-radius: 6px; font-family: monospace; font-size: 12px; color: #6b7280;">
|
||||
Hash: {{ original.hash }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Servers & Links Management -->
|
||||
<div style="display: grid; gap: 20px;">
|
||||
{% for server_name, data in servers_data.items %}
|
||||
<div style="background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h4 style="margin: 0; color: #1f2937; font-size: 16px;">
|
||||
{% if data.server.server_type == 'outline' %}🔵{% elif data.server.server_type == 'wireguard' %}🟢{% else %}⚪{% endif %}
|
||||
{{ server_name }}
|
||||
</h4>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
{% if data.accessible %}
|
||||
<span style="background: #dcfce7; color: #166534; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
|
||||
✅ Online
|
||||
</span>
|
||||
{% else %}
|
||||
<span style="background: #fef2f2; color: #dc2626; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
|
||||
❌ Offline
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Server Statistics -->
|
||||
{% for stat in data.statistics %}
|
||||
{% if not stat.acl_link_id %}
|
||||
<span style="background: #f3f4f6; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
|
||||
📊 {{ stat.total_connections }} uses
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if data.error %}
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; padding: 12px; margin-bottom: 16px;">
|
||||
<div style="color: #dc2626; font-size: 14px;">⚠️ Server Error:</div>
|
||||
<div style="color: #7f1d1d; font-size: 12px; margin-top: 4px;">{{ data.error }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Server Status Info -->
|
||||
{% if data.status %}
|
||||
<div style="background: #f8fafc; border-radius: 6px; padding: 12px; margin-bottom: 16px;">
|
||||
<div style="font-weight: bold; color: #374151; margin-bottom: 8px;">Server Status:</div>
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px;">
|
||||
{% for key, value in data.status.items %}
|
||||
<span><strong>{{ key }}:</strong> {{ value }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Links for this server -->
|
||||
<div>
|
||||
<div style="font-weight: bold; color: #374151; margin-bottom: 12px;">
|
||||
Access Links ({{ data.links|length }}):
|
||||
</div>
|
||||
|
||||
{% if data.links %}
|
||||
<div style="display: grid; gap: 12px;">
|
||||
{% for link in data.links %}
|
||||
<div class="link-container" style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div style="display: flex; gap: 12px; align-items: center; flex: 1;">
|
||||
<span style="font-family: monospace; font-size: 14px; color: #2563eb; font-weight: bold;">
|
||||
{{ link.link|slice:":16" }}{% if link.link|length > 16 %}...{% endif %}
|
||||
</span>
|
||||
<span style="color: #6b7280; font-size: 12px;">
|
||||
{{ link.comment|default:"No comment" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Link Statistics -->
|
||||
{% for stat in data.statistics %}
|
||||
{% if stat.acl_link_id == link.link %}
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<span style="background: #eff6ff; color: #1d4ed8; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
||||
✨ {{ stat.total_connections }} total
|
||||
</span>
|
||||
<span style="background: #f0fdf4; color: #166534; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
||||
📅 {{ stat.recent_connections }} recent
|
||||
</span>
|
||||
{% if stat.max_daily > 0 %}
|
||||
<span style="background: #fef3c7; color: #d97706; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
||||
📈 {{ stat.max_daily }} peak
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<a href="{{ external_address }}/ss/{{ link.link }}#{{ server_name }}" target="_blank"
|
||||
style="background: #3b82f6; color: white; padding: 4px 8px; border-radius: 4px; text-decoration: none; font-size: 11px;">
|
||||
🔗 Test Link
|
||||
</a>
|
||||
|
||||
<button type="button" class="delete-link-btn"
|
||||
data-link-id="{{ link.id }}" data-link-name="{{ link.link|slice:":16" }}"
|
||||
style="background: #dc2626; color: white; padding: 4px 8px; border-radius: 4px; border: none; font-size: 11px; cursor: pointer;">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
|
||||
{% if link.last_access_time %}
|
||||
<span style="background: #f3f4f6; padding: 4px 8px; border-radius: 4px; font-size: 11px; color: #6b7280;">
|
||||
Last used: {{ link.last_access_time|date:"Y-m-d H:i" }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span style="background: #fef2f2; color: #dc2626; padding: 4px 8px; border-radius: 4px; font-size: 11px;">
|
||||
Never used
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color: #6b7280; font-style: italic; text-align: center; padding: 20px; background: #f9fafb; border-radius: 6px;">
|
||||
No access links configured for this server
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add New Link Button -->
|
||||
<div style="margin-top: 16px; text-align: center;">
|
||||
<button type="button" class="add-link-btn"
|
||||
data-server-id="{{ data.server.id }}" data-server-name="{{ server_name }}"
|
||||
style="background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; border: none; font-size: 12px; cursor: pointer; font-weight: bold;">
|
||||
➕ Add New Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Add Server Access -->
|
||||
{% if unassigned_servers %}
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 16px; margin-top: 20px;">
|
||||
<h4 style="margin: 0 0 12px 0; color: #166534; font-size: 14px;">➕ Available Servers</h4>
|
||||
<div style="color: #166534; font-size: 12px; margin-bottom: 12px;">
|
||||
Click to instantly add access to these servers:
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
{% for server in unassigned_servers %}
|
||||
<button type="button" class="add-server-btn"
|
||||
data-server-id="{{ server.id }}" data-server-name="{{ server.name }}"
|
||||
style="background: #dcfce7; color: #166534; padding: 6px 12px; border-radius: 4px; font-size: 12px; border: 1px solid #bbf7d0; cursor: pointer; font-weight: bold;">
|
||||
{% if server.server_type == 'outline' %}🔵{% elif server.server_type == 'wireguard' %}🟢{% else %}⚪{% endif %}
|
||||
{{ server.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Recent Activity Panel -->
|
||||
{% if original and recent_logs %}
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 24px 0;">
|
||||
<h3 style="margin: 0 0 20px 0; color: #1f2937; font-size: 18px;">📈 Recent Activity (Last 30 days)</h3>
|
||||
|
||||
<div style="background: #fff; border-radius: 6px; overflow: hidden; border: 1px solid #e5e7eb;">
|
||||
<div style="background: #f9fafb; padding: 12px; border-bottom: 1px solid #e5e7eb; font-weight: bold; font-size: 14px;">
|
||||
Activity Log
|
||||
</div>
|
||||
|
||||
<div style="max-height: 400px; overflow-y: auto;">
|
||||
{% for log in recent_logs %}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px; border-bottom: 1px solid #f3f4f6;">
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
{% if log.action == 'Success' %}
|
||||
<span style="color: #16a34a; font-size: 16px;">✅</span>
|
||||
{% elif log.action == 'Failed' %}
|
||||
<span style="color: #dc2626; font-size: 16px;">❌</span>
|
||||
{% else %}
|
||||
<span style="color: #6b7280; font-size: 16px;">ℹ️</span>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<div style="font-weight: 500; font-size: 14px;">{{ log.server }}</div>
|
||||
{% if log.acl_link_id %}
|
||||
<div style="font-family: monospace; font-size: 12px; color: #6b7280;">
|
||||
{{ log.acl_link_id|slice:":16" }}{% if log.acl_link_id|length > 16 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 12px; color: #6b7280;">
|
||||
{{ log.timestamp|date:"Y-m-d H:i:s" }}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #9ca3af;">
|
||||
{{ log.action }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.btn-hover:hover {
|
||||
opacity: 0.8;
|
||||
transform: translateY(-1px);
|
||||
.user-management-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.btn-loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
|
||||
.user-management-section h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: #495057;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.server-section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.link-item {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-sm-custom {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.2rem;
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
|
||||
.readonly .user-management-section {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_change_form_document_ready %}
|
||||
{{ block.super }}
|
||||
{% if original %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userId = {{ original.id|default:"null" }};
|
||||
const userId = {{ original.id }};
|
||||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Show success/error messages
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertClass = type === 'error' ? 'alert-danger' : 'alert-success';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="margin: 16px 0;">
|
||||
// Show success/error messages in Django admin style
|
||||
function showMessage(message, type = 'success') {
|
||||
const messageClass = type === 'error' ? 'error' : 'success';
|
||||
const messageHtml = `
|
||||
<div class="alert alert-${messageClass} alert-dismissible" style="margin: 1rem 0;">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<button type="button" class="close" aria-label="Close" onclick="this.parentElement.remove()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Find a good place to insert the message
|
||||
const target = document.querySelector('.user-access-management') || document.querySelector('.content');
|
||||
const target = document.querySelector('.card-body') || document.querySelector('.content');
|
||||
if (target) {
|
||||
target.insertAdjacentHTML('afterbegin', alertHtml);
|
||||
// Auto-remove after 5 seconds
|
||||
target.insertAdjacentHTML('afterbegin', messageHtml);
|
||||
setTimeout(() => {
|
||||
const alert = target.querySelector('.alert');
|
||||
if (alert) alert.remove();
|
||||
@@ -291,11 +100,11 @@
|
||||
const serverName = this.dataset.serverName;
|
||||
|
||||
const comment = prompt(`Add comment for new link on ${serverName} (optional):`, '');
|
||||
if (comment === null) return; // User cancelled
|
||||
if (comment === null) return;
|
||||
|
||||
const originalText = this.textContent;
|
||||
this.textContent = '⏳ Adding...';
|
||||
this.classList.add('btn-loading');
|
||||
this.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/vpn/user/${userId}/add-link/`, {
|
||||
@@ -310,9 +119,8 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage(`✅ New link created successfully: ${data.link}`, 'success');
|
||||
// Refresh the page to show the new link
|
||||
window.location.reload();
|
||||
showMessage(`✅ New link created successfully: ${data.link}`);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
showMessage(`❌ Error: ${data.error}`, 'error');
|
||||
}
|
||||
@@ -320,7 +128,7 @@
|
||||
showMessage(`❌ Network error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this.textContent = originalText;
|
||||
this.classList.remove('btn-loading');
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -337,7 +145,7 @@
|
||||
|
||||
const originalText = this.textContent;
|
||||
this.textContent = '⏳ Deleting...';
|
||||
this.classList.add('btn-loading');
|
||||
this.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/vpn/user/${userId}/delete-link/${linkId}/`, {
|
||||
@@ -351,9 +159,8 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage(`✅ Link ${linkName} deleted successfully`, 'success');
|
||||
// Remove the link element from DOM
|
||||
this.closest('.link-container')?.remove() || window.location.reload();
|
||||
showMessage(`✅ Link ${linkName} deleted successfully`);
|
||||
this.closest('.link-item')?.remove();
|
||||
} else {
|
||||
showMessage(`❌ Error: ${data.error}`, 'error');
|
||||
}
|
||||
@@ -361,7 +168,7 @@
|
||||
showMessage(`❌ Network error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this.textContent = originalText;
|
||||
this.classList.remove('btn-loading');
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -378,7 +185,7 @@
|
||||
|
||||
const originalText = this.textContent;
|
||||
this.textContent = '⏳ Adding...';
|
||||
this.classList.add('btn-loading');
|
||||
this.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/vpn/user/${userId}/add-server-access/`, {
|
||||
@@ -393,9 +200,8 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage(`✅ Access to ${serverName} added successfully`, 'success');
|
||||
// Refresh the page to show the new server section
|
||||
window.location.reload();
|
||||
showMessage(`✅ Access to ${serverName} added successfully`);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
showMessage(`❌ Error: ${data.error}`, 'error');
|
||||
}
|
||||
@@ -403,10 +209,11 @@
|
||||
showMessage(`❌ Network error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this.textContent = originalText;
|
||||
this.classList.remove('btn-loading');
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user