Files
OutFleet/vpn/templates/admin/move_clients.html

500 lines
20 KiB
HTML
Raw Permalink Normal View History

2025-06-27 16:36:02 +03:00
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block content %}
<div class="content-main">
<h1>{{ title }}</h1>
2025-06-27 17:08:32 +03:00
<div class="alert alert-info" style="margin: 10px 0; padding: 10px; border-radius: 4px; background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;">
<strong>Note:</strong> This operation only affects the database and works even if servers are unreachable.
Server connectivity is not required for moving links between servers.
</div>
2025-06-27 16:36:02 +03:00
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" style="margin: 10px 0; padding: 10px; border-radius: 4px;
{% if message.tags == 'error' %}background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;
{% elif message.tags == 'success' %}background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724;
{% elif message.tags == 'warning' %}background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404;
{% elif message.tags == 'info' %}background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;
{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="post" id="move-clients-form">
{% csrf_token %}
<div class="form-row" style="margin-bottom: 20px;">
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<label for="source_server"><strong>Source Server:</strong></label>
<select id="source_server" name="source_server" style="width: 100%; padding: 5px;" onchange="updateLinksList()">
<option value="">-- Select Source Server --</option>
{% for server in servers %}
<option value="{{ server.id }}">{{ server.name }} ({{ server.server_type }})</option>
{% endfor %}
</select>
</div>
<div style="flex: 1;">
<label for="target_server"><strong>Target Server:</strong></label>
<select id="target_server" name="target_server" style="width: 100%; padding: 5px;" onchange="updateSubmitButton()">
<option value="">-- Select Target Server --</option>
{% for server in all_servers %}
<option value="{{ server.id }}">{{ server.name }} ({{ server.server_type }})</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div id="links-list" style="margin-bottom: 20px;">
<h3>Select Client Links to Move:</h3>
<div id="links-container">
<p style="color: #666;">Please select a source server first.</p>
</div>
</div>
2025-06-27 17:21:54 +03:00
<div class="form-row" style="margin-bottom: 20px;">
<h3>Comment Transformation (Optional)</h3>
<div style="margin-bottom: 10px;">
2025-06-27 17:08:32 +03:00
<label for="comment_regex"><strong>Regular Expression Pattern:</strong></label>
<input type="text" id="comment_regex" name="comment_regex"
2025-06-27 17:21:54 +03:00
style="width: 100%; padding: 8px; font-family: monospace;"
2025-06-27 17:08:32 +03:00
placeholder="Example: ^(.*)$ -> [OLD_SERVER] $1"
title="Use regex pattern -> replacement format">
</div>
2025-06-27 17:21:54 +03:00
<div class="regex-help" style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; border-left: 4px solid #007cba;">
<h4 style="margin: 0 0 10px 0; color: #007cba; font-size: 14px;">Regular Expression Help & Examples:</h4>
<div style="font-size: 13px;">
<p style="margin: 0 0 8px 0;"><strong>Format:</strong> <code>pattern -> replacement</code></p>
<div style="margin-bottom: 10px;">
<h5 style="margin: 0 0 5px 0; font-size: 13px;">Common Examples:</h5>
<ul style="margin: 0; padding-left: 15px; line-height: 1.4;">
<li><code>^(.*)$ -> [FROM RU] $1</code> <small>- Add prefix to all comments</small></li>
<li><code>^(.*)$ -> $1 (moved)</code> <small>- Add suffix to all comments</small></li>
<li><code>^$ -> Default Device</code> <small>- Replace empty comments</small></li>
<li><code>phone -> mobile</code> <small>- Replace specific word</small></li>
</ul>
</div>
2025-06-27 17:08:32 +03:00
2025-06-27 17:21:54 +03:00
<div style="margin-bottom: 10px;">
<h5 style="margin: 0 0 5px 0; font-size: 13px;">Advanced Examples:</h5>
<ul style="margin: 0; padding-left: 15px; line-height: 1.4;">
<li><code>^(.+) - (.+)$ -> $2: $1</code> <small>- Swap parts separated by " - "</small></li>
<li><code>(\d{4})-(\d{2})-(\d{2}) -> $3/$2/$1</code> <small>- Change date format</small></li>
<li><code>^(.{1,20}).*$ -> $1...</code> <small>- Truncate long comments</small></li>
</ul>
2025-06-27 17:08:32 +03:00
</div>
2025-06-27 17:21:54 +03:00
<div style="padding: 8px; background-color: #fff3cd; border-radius: 3px; margin: 0;">
<strong style="font-size: 12px;">Tips:</strong>
<ul style="margin: 3px 0 0 0; padding-left: 15px; font-size: 12px; line-height: 1.3;">
<li>Use <code>$1, $2, $3...</code> for captured groups (auto-converted to Python)</li>
<li>Use <code>^</code> for start, <code>$</code> for end of string</li>
<li>Preview shows result; leave empty to keep original comments</li>
2025-06-27 17:08:32 +03:00
</ul>
</div>
</div>
</div>
</div>
2025-06-27 16:36:02 +03:00
<div class="submit-row">
<input type="submit" value="Move Selected Links" class="default" id="submit-btn" disabled>
<a href="{% url 'admin:vpn_server_changelist' %}" class="button cancel">Cancel</a>
</div>
</form>
</div>
<script>
// Client links data for each server
var linksByServer = {
{% for server, links in links_by_server.items %}
"{{ server.id }}": [
{% for link in links %}
{
"id": {{ link.id }},
"link": "{{ link.link|escapejs }}",
"comment": "{{ link.comment|escapejs }}",
"username": "{{ link.acl.user.username|escapejs }}",
"user_comment": "{{ link.acl.user.comment|escapejs }}",
"created_at": "{{ link.acl.created_at|date:'Y-m-d H:i' }}",
"full_url": "{{ EXTERNAL_ADDRESS }}/ss/{{ link.link }}#{{ link.acl.server.name|escapejs }}"
},
{% endfor %}
],
{% endfor %}
};
function updateLinksList() {
var sourceServerId = document.getElementById('source_server').value;
var linksContainer = document.getElementById('links-container');
var submitBtn = document.getElementById('submit-btn');
if (!sourceServerId) {
linksContainer.innerHTML = '<p style="color: #666;">Please select a source server first.</p>';
submitBtn.disabled = true;
return;
}
var links = linksByServer[sourceServerId] || [];
if (links.length === 0) {
linksContainer.innerHTML = '<p style="color: #666;">No client links found for this server.</p>';
submitBtn.disabled = true;
return;
}
// Group links by user for better organization
var linksByUser = {};
links.forEach(function(link) {
if (!linksByUser[link.username]) {
linksByUser[link.username] = {
user_comment: link.user_comment,
created_at: link.created_at,
links: []
};
}
linksByUser[link.username].links.push(link);
});
var html = '<div style="max-height: 500px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">';
html += '<div style="margin-bottom: 10px;">';
html += '<button type="button" onclick="toggleAllLinks()" style="padding: 5px 10px; margin-right: 10px;">Select All</button>';
html += '<button type="button" onclick="toggleAllLinks(false)" style="padding: 5px 10px;">Deselect All</button>';
html += '</div>';
html += '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background-color: #f5f5f5;">';
html += '<th style="padding: 8px; border: 1px solid #ddd; width: 50px;">Select</th>';
html += '<th style="padding: 8px; border: 1px solid #ddd;">Username</th>';
html += '<th style="padding: 8px; border: 1px solid #ddd;">Link Comment</th>';
html += '<th style="padding: 8px; border: 1px solid #ddd;">Link ID</th>';
html += '<th style="padding: 8px; border: 1px solid #ddd;">User Comment</th>';
html += '<th style="padding: 8px; border: 1px solid #ddd;">Created</th>';
html += '</tr></thead><tbody>';
// Sort users alphabetically
var sortedUsers = Object.keys(linksByUser).sort();
sortedUsers.forEach(function(username) {
var userData = linksByUser[username];
var userLinks = userData.links;
// Add user row header if user has multiple links
if (userLinks.length > 1) {
html += '<tr style="background-color: #f9f9f9;">';
html += '<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">';
html += '<input type="checkbox" class="user-checkbox" data-username="' + username + '" onchange="toggleUserLinks(\'' + username + '\')">';
html += '</td>';
html += '<td colspan="5" style="padding: 8px; border: 1px solid #ddd;"><strong>' + username + '</strong> (' + userLinks.length + ' links)</td>';
html += '</tr>';
}
// Add individual link rows
userLinks.forEach(function(link) {
html += '<tr class="link-row" data-username="' + username + '">';
html += '<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">';
2025-06-27 17:21:54 +03:00
html += '<input type="checkbox" name="selected_links" value="' + link.id + '" class="link-checkbox" data-username="' + username + '" onchange="updateSubmitButton(); updateUserCheckbox(\'' + username + '\')">';
2025-06-27 16:36:02 +03:00
html += '</td>';
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (userLinks.length === 1 ? '<strong>' + username + '</strong>' : '↳') + '</td>';
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (link.comment || '<em>No comment</em>') + '</td>';
html += '<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace; font-size: 12px;">' + link.link + '</td>';
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + (userData.user_comment || '') + '</td>';
html += '<td style="padding: 8px; border: 1px solid #ddd;">' + userData.created_at + '</td>';
html += '</tr>';
});
});
html += '</tbody></table></div>';
linksContainer.innerHTML = html;
updateSubmitButton();
}
function toggleAllLinks(selectAll = true) {
var checkboxes = document.getElementsByName('selected_links');
var userCheckboxes = document.getElementsByClassName('user-checkbox');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = selectAll;
}
for (var i = 0; i < userCheckboxes.length; i++) {
userCheckboxes[i].checked = selectAll;
}
updateSubmitButton();
}
function toggleUserLinks(username) {
var userCheckbox = document.querySelector('.user-checkbox[data-username="' + username + '"]');
var userLinkCheckboxes = document.querySelectorAll('.link-checkbox[data-username="' + username + '"]');
for (var i = 0; i < userLinkCheckboxes.length; i++) {
userLinkCheckboxes[i].checked = userCheckbox.checked;
}
updateSubmitButton();
}
function updateUserCheckbox(username) {
var userLinkCheckboxes = document.querySelectorAll('.link-checkbox[data-username="' + username + '"]');
var userCheckbox = document.querySelector('.user-checkbox[data-username="' + username + '"]');
if (!userCheckbox) return; // No user checkbox for single-link users
var allChecked = true;
var noneChecked = true;
for (var i = 0; i < userLinkCheckboxes.length; i++) {
if (userLinkCheckboxes[i].checked) {
noneChecked = false;
} else {
allChecked = false;
}
}
if (allChecked) {
userCheckbox.checked = true;
userCheckbox.indeterminate = false;
} else if (noneChecked) {
userCheckbox.checked = false;
userCheckbox.indeterminate = false;
} else {
userCheckbox.checked = false;
userCheckbox.indeterminate = true;
}
}
function updateSubmitButton() {
var checkboxes = document.getElementsByName('selected_links');
var sourceServer = document.getElementById('source_server').value;
var targetServer = document.getElementById('target_server').value;
var submitBtn = document.getElementById('submit-btn');
var hasSelected = false;
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked) {
hasSelected = true;
break;
}
}
submitBtn.disabled = !(hasSelected && sourceServer && targetServer && sourceServer !== targetServer);
}
2025-06-27 17:21:54 +03:00
// Form submission confirmation
document.getElementById('move-clients-form').addEventListener('submit', function(e) {
var checkboxes = document.getElementsByName('selected_links');
var selectedCount = 0;
var selectedUsers = new Set();
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked) {
selectedCount++;
selectedUsers.add(checkboxes[i].getAttribute('data-username'));
}
}
var sourceServerName = document.getElementById('source_server').selectedOptions[0].text;
var targetServerName = document.getElementById('target_server').selectedOptions[0].text;
var commentRegex = document.getElementById('comment_regex').value.trim();
var confirmMessage = 'Are you sure you want to move ' + selectedCount + ' link(s) for ' + selectedUsers.size + ' user(s) from "' + sourceServerName + '" to "' + targetServerName + '"?\n\n';
confirmMessage += 'This action will:\n';
confirmMessage += '- Transfer selected links to target server\n';
confirmMessage += '- Create ACLs for users who don\'t have access to target server\n';
confirmMessage += '- Remove empty ACLs from source server\n';
confirmMessage += '- Preserve all link settings and comments\n';
if (commentRegex) {
confirmMessage += '- Apply regex transformation to comments: "' + commentRegex + '"\n';
}
confirmMessage += '\nThis cannot be undone.';
if (!confirm(confirmMessage)) {
e.preventDefault();
}
});
2025-06-27 17:08:32 +03:00
// Add regex preview functionality
document.getElementById('comment_regex').addEventListener('input', function() {
updateRegexPreview();
});
function updateRegexPreview() {
var regexInput = document.getElementById('comment_regex').value.trim();
// Remove any existing preview
var existingPreview = document.getElementById('regex-preview');
if (existingPreview) {
existingPreview.remove();
}
if (!regexInput) return;
// Parse pattern -> replacement format
var parts = regexInput.split(' -> ');
if (parts.length !== 2) {
showRegexError('Invalid format. Use: pattern -> replacement');
return;
}
var pattern = parts[0];
var replacement = parts[1];
try {
var regex = new RegExp(pattern, 'g');
2025-06-27 17:21:54 +03:00
// Test with sample comments from currently visible links
2025-06-27 17:08:32 +03:00
var sampleComments = getSampleComments();
if (sampleComments.length > 0) {
showRegexPreview(sampleComments, regex, replacement);
}
} catch (e) {
showRegexError('Invalid regex pattern: ' + e.message);
}
}
function getSampleComments() {
var comments = [];
var checkboxes = document.getElementsByName('selected_links');
2025-06-27 17:21:54 +03:00
// Collect actual comments from visible links
2025-06-27 17:08:32 +03:00
for (var i = 0; i < checkboxes.length && comments.length < 5; i++) {
2025-06-27 17:21:54 +03:00
var row = checkboxes[i].closest('tr');
if (row) {
2025-06-27 17:08:32 +03:00
var commentCell = row.children[2]; // Link Comment column
2025-06-27 17:21:54 +03:00
if (commentCell) {
var commentText = commentCell.textContent.trim();
if (commentText && commentText !== 'No comment') {
comments.push(commentText);
}
2025-06-27 17:08:32 +03:00
}
}
}
2025-06-27 17:21:54 +03:00
// Add some realistic default samples if no comments found or need more samples
2025-06-27 17:23:05 +03:00
var defaultSamples = ['iPhone 13'];
2025-06-27 17:21:54 +03:00
for (var i = 0; i < defaultSamples.length && comments.length < 5; i++) {
if (comments.indexOf(defaultSamples[i]) === -1) {
comments.push(defaultSamples[i]);
}
2025-06-27 17:08:32 +03:00
}
2025-06-27 17:21:54 +03:00
return comments.slice(0, 5); // Limit to 5 samples
2025-06-27 17:08:32 +03:00
}
function showRegexPreview(samples, regex, replacement) {
2025-06-27 17:21:54 +03:00
var previewHtml = '<div id="regex-preview" style="margin-top: 10px; padding: 10px; background-color: #e8f5e8; border-radius: 3px; border-left: 4px solid #28a745;">';
previewHtml += '<h5 style="margin-top: 0; color: #28a745;">Preview (first 5 samples):</h5>';
previewHtml += '<table style="width: 100%; font-size: 13px;">';
previewHtml += '<tr><th style="text-align: left; padding: 5px;">Original</th><th style="text-align: left; padding: 5px;"></th><th style="text-align: left; padding: 5px;">Transformed</th></tr>';
2025-06-27 17:08:32 +03:00
samples.forEach(function(comment) {
var original = comment || '(empty)';
2025-06-27 17:21:54 +03:00
var transformed;
try {
// Use string replace with regex and replacement string
transformed = original.replace(regex, replacement);
} catch (e) {
transformed = '(error: ' + e.message + ')';
}
2025-06-27 17:08:32 +03:00
var changed = original !== transformed;
previewHtml += '<tr>';
2025-06-27 17:21:54 +03:00
previewHtml += '<td style="padding: 3px 5px; font-family: monospace;">' + escapeHtml(original) + '</td>';
previewHtml += '<td style="padding: 3px 5px;"></td>';
previewHtml += '<td style="padding: 3px 5px; font-family: monospace;' + (changed ? ' font-weight: bold; color: #28a745;' : '') + '">' + escapeHtml(transformed) + '</td>';
2025-06-27 17:08:32 +03:00
previewHtml += '</tr>';
});
previewHtml += '</table></div>';
document.querySelector('.regex-help').insertAdjacentHTML('afterend', previewHtml);
}
2025-06-27 17:21:54 +03:00
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
2025-06-27 17:08:32 +03:00
function showRegexError(message) {
2025-06-27 17:21:54 +03:00
var errorHtml = '<div id="regex-preview" style="margin-top: 10px; padding: 10px; background-color: #f8d7da; border-radius: 3px; border-left: 4px solid #dc3545;">';
errorHtml += '<h5 style="margin-top: 0; color: #dc3545;">Error:</h5>';
errorHtml += '<p style="margin: 0; color: #721c24;">' + message + '</p>';
2025-06-27 17:08:32 +03:00
errorHtml += '</div>';
document.querySelector('.regex-help').insertAdjacentHTML('afterend', errorHtml);
}
2025-06-27 16:36:02 +03:00
</script>
<style>
.alert {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
.form-row {
margin-bottom: 20px;
}
.submit-row {
padding: 12px 14px;
margin: 0 0 20px;
background: #f8f8f8;
border: 1px solid #ddd;
border-radius: 4px;
}
.submit-row input[type="submit"] {
background: #417690;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.submit-row input[type="submit"]:disabled {
background: #ccc;
cursor: not-allowed;
}
.submit-row .cancel {
background: #6c757d;
color: white;
padding: 10px 15px;
border-radius: 4px;
text-decoration: none;
}
.submit-row .cancel:hover {
background: #5a6268;
color: white;
}
.link-row:hover {
background-color: #f8f9fa;
}
input[type="checkbox"]:indeterminate {
opacity: 0.5;
}
</style>
{% endblock %}