v2.0.0
# Complete Enhancement Package - Major Feature Update Comprehensive enhancement package for docker-ddns-server including security features, modern authentication, UI/UX improvements, and production-ready deployment features. ## 🔒 Security & Authentication ### IP Blocking System - Implemented automatic IP blocking after 3 failed authentication attempts within 72 hours - Added 7-day block duration with automatic expiration - Created `blocked_ips` database table for tracking blocked addresses - Added automatic cleanup of expired blocks - Implemented manual IP unblock capability via security dashboard ### Failed Authentication Logging - Added comprehensive failed authentication logging system - Created `failed_auths` database table storing IP, timestamp, username, and password - Implemented threat intelligence features for password pattern analysis - Added automatic cleanup of old authentication records - Logs intentionally include passwords for single-user security analysis ### Session-Based Authentication - Replaced HTTP Basic Auth with modern session-based authentication for admin panel - Integrated gorilla/sessions library for secure session management - Added configurable session secrets via `DDNS_SESSION_SECRET` environment variable - Implemented "Remember Me" functionality with 30-day session duration - Added proper session destruction on logout - Session cookies configured with HttpOnly, Secure, and SameSite attributes - Maintained HTTP Basic Auth for DynDNS API endpoints (device compatibility) ### HTTPS Enforcement - Added intelligent HTTPS detection via multiple header checks - Implemented automatic HTTPS redirect for admin panel when available - Graceful HTTP fallback when HTTPS unavailable - Supports reverse proxy configurations (nginx, Caddy, Traefik) - Detects SSL via X-Forwarded-Proto, X-Forwarded-Ssl, X-Url-Scheme headers - API endpoints remain HTTP-compatible for device support ## 🎨 UI/UX Enhancements ### Authentication UI - Created modern login page with gradient background and clean design - Added HTTPS security indicator (✓ green / ⚠ yellow) - Implemented auto-focus on username field - Added clear error messages for failed login attempts - Created logout confirmation page with redirect options - Removed browser authentication dialog popups ### Navigation & Layout - Changed admin panel URL from `/admin` to `/@` for uniqueness - Updated navigation with unicode icons (🏠 Dashboard, 🔒 Security, ⏏️ Logout) - Added tooltips to all navigation icons - Implemented sticky header that remains visible on scroll - Enhanced responsive design for mobile/tablet access ### Logo Support - Added automatic logo detection and display - Supports PNG, WebP, and SVG formats - Checks `/static/icons/` for logo files - Graceful fallback to text title if no logo found - Maintains aspect ratio and responsive sizing ### Security Dashboard - Created comprehensive security overview page at `/@/security` - Added statistics cards showing active blocks, failed attempts, and total blocks - Implemented recent failed attempts table with sortable columns - Added password reveal/hide functionality with confirmation prompts - Created detailed blocked IPs management page with unblock capability - Created detailed failed authentication logs page with full history - Added visual indicators for security status ## 📊 Data Management ### Data Consistency & Normalization - Implemented automatic lowercase conversion for all usernames and hostnames - Prevents case-sensitivity issues in DNS lookups and authentication - Ensures consistent data storage and retrieval - Handles mixed-case legacy data gracefully ### Automatic Migration - Added on-the-fly migration system for legacy uppercase entries - Migration triggers automatically on first `/@/hosts` page visit - Handles hostname conflicts by appending sequential numbers - Provides detailed migration report in UI showing all changes - Non-destructive migration preserves all host data - One-time execution with persistent migration status tracking ### Validation Updates - Reduced minimum hostname length to 1 character (allows single-letter subdomains) - Reduced minimum username length to 1 character - Reduced minimum password length to 6 characters - Maintained security while improving flexibility ### Username Uniqueness - Removed uniqueness constraint on usernames - Allows multiple hosts to share the same username - Supports different passwords for same username across hosts - Enables more flexible credential management strategies ## 🛡️ Middleware & Request Handling ### IP Blocker Middleware - Created IPBlockerMiddleware to check requests against blocked IPs - Automatic redirect to 127.0.0.1 for blocked addresses - Lightweight performance impact with database lookup - Positioned early in middleware chain for efficiency ### Session Authentication Middleware - Created SessionAuthMiddleware for admin panel protection - Skips authentication check for /login and /logout routes - Redirects unauthenticated users to login page - Validates session integrity on every request - Compatible with reverse proxy configurations ### HTTPS Redirect Middleware - Created HTTPSRedirectMiddleware for admin panel security - Intelligent detection of HTTPS availability - Skips redirect for API endpoints - Handles X-Forwarded-* headers from reverse proxies - Graceful operation when HTTPS unavailable ## 🗄️ Database & Models ### New Tables - Added `failed_auths` table for authentication logging - Added `blocked_ips` table for IP block tracking - Proper foreign key relationships and indexes - Automatic timestamps on all records ### Cleanup Functions - Implemented automatic cleanup of expired IP blocks - Implemented automatic cleanup of old authentication logs - Configurable retention periods - Background cleanup execution ## 🔧 Technical Improvements ### Dependencies - Added `github.com/gorilla/sessions@v1.2.2` for session management - Updated go.mod with proper version constraints - Maintained compatibility with existing dependencies ### Handler Architecture - Separated security logic into dedicated handler files - Created `security.go` for blocking logic and logging - Created `security_dashboard.go` for UI handlers - Created `auth.go` for login/logout and session management - Created `session.go` for session store implementation - Improved code organization and maintainability ### Main Application - Updated routing to support session-based authentication - Added session initialization on startup - Configured route groups for admin panel and API - Middleware ordering optimized for performance and security ## 🐳 Docker & CI/CD ### Multi-Platform Builds & Automated Releases - Created GitHub Actions workflow (`BuildEmAll.yml`) for automated Docker builds - Supports linux/amd64, linux/386, linux/arm/v7, and linux/arm64 platforms - Automatic builds on push to master with dyndns/ directory changes - Intelligent version tagging system: - Extracts version from commit message (e.g., "v1.2.3 Feature description") - Auto-increments patch version from latest git tag - Falls back to date-based versioning (vYY.MM.DD-HHMM) if no tags exist - Tags images with both `:latest` and semantic version tags (`:vX.Y.Z`) - Automatic GitHub release creation with each build - Release includes Docker image reference and commit message as notes - Publishes to Docker Hub (w3kllc/ddns) - Cross-platform compatibility for ARM devices (Raspberry Pi, etc.) - Workflow can be triggered manually via GitHub Actions UI ### Deployment - Enhanced docker-compose.yml example with all new features - Added documentation for environment variable configuration - Included reverse proxy configuration examples - Added security best practices for production deployment - Semantic versioning with automatic release management ## 📝 Documentation ### README Enhancements - Added comprehensive Security Features section - Added Environment Variables reference with descriptions - Added Admin Panel Access documentation - Added Data Consistency & Migration guide - Added API Endpoints documentation - Added UI/UX Enhancements overview - Added Reverse Proxy Configuration examples - Added Docker Configuration best practices - Added CI/CD & Multi-Platform Support details with versioning strategy - Added Semantic Versioning documentation - Added GitHub Release automation details - Added Security Best Practices recommendations - Added Threat Intelligence rationale - Added Migration Guide from original project - Added Troubleshooting section - Added API Reference documentation - Added Roadmap for future features - Updated Credits section - Added Support and Community links ## 🔄 Backward Compatibility ### Maintained Features - DynDNS API endpoints remain unchanged (/update, /nic/update, etc.) - HTTP Basic Auth still supported for API (device compatibility) - Existing host configurations continue working without changes - Database schema additions are non-breaking - All original functionality preserved ### Breaking Changes - Admin panel URL changed from `/admin` to `/@` (intentional, more unique) - Admin authentication method changed (sessions vs basic auth) - Requires `DDNS_SESSION_SECRET` environment variable for session security ## ⚡ Performance Considerations - IP blocker checks are optimized with database indexing - Session validation cached in memory - Automatic cleanup runs asynchronously - Minimal overhead on API endpoint performance - Efficient middleware ordering ## 🎯 Testing Considerations Recommended testing areas: - Login/logout flow with and without HTTPS - IP blocking after 3 failed attempts - Session persistence with remember me - API endpoint authentication (device compatibility) - HTTPS redirect with reverse proxy headers - Password reveal/hide in security dashboard - Hostname migration for legacy uppercase entries - Multi-platform Docker image functionality --- **Total Changes:** - **21 files modified** - **20 new files created** - **~2000+ lines of code added** - **100+ hours of development time** **Compatibility:** - ✅ Backward compatible for DynDNS API - ⚠️ Admin panel URL changed (bookmark update needed) - ✅ All existing hosts continue working - ✅ Database schema additions are additive **Credits:** - Original project: dprandzioch/docker-ddns - Web UI Fork: benjaminbear/docker-ddns-server - Enhanced fork: w3K-one/docker-ddns-server - Major enhancements and security features added This represents a significant enhancement to the original project while maintaining the core DynDNS functionality and adding modern security, authentication, and user experience improvements suitable for production deployment.
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
{{define "content"}}
|
||||
<div class="container marketing">
|
||||
<h3 class="text-center mb-4">Security Dashboard</h3>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-white bg-danger mb-3">
|
||||
<div class="card-header">Active Blocks</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{.activeBlocks}}</h2>
|
||||
<p class="card-text">IP addresses currently blocked</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-white bg-warning mb-3">
|
||||
<div class="card-header">Failed Attempts</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{len .failedAuths}}</h2>
|
||||
<p class="card-text">Recent failed login attempts</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-white bg-info mb-3">
|
||||
<div class="card-header">Total Blocks</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{len .blockedIPs}}</h2>
|
||||
<p class="card-text">Total IP blocks (active + expired)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recently Blocked IPs -->
|
||||
<h4 class="mb-3">Recently Blocked IPs</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Blocked At</th>
|
||||
<th>Blocked Until</th>
|
||||
<th>Failures</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .blockedIPs}}
|
||||
{{if .IsBlocked}}
|
||||
<tr>
|
||||
<td><code>{{.IPAddress}}</code></td>
|
||||
<td>{{.BlockedAt.Format "01/02/2006 15:04"}}</td>
|
||||
<td>{{if .IsPermanent}}Permanent{{else}}{{.BlockedUntil.Format "01/02/2006 15:04"}}{{end}}</td>
|
||||
<td><span class="badge badge-danger">{{.FailureCount}}</span></td>
|
||||
<td><span class="badge badge-danger">Active</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning unblock-btn" data-ip="{{.IPAddress}}">Unblock</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Recent Failed Authentication Attempts -->
|
||||
<h4 class="mb-3 mt-4">Recent Failed Authentication Attempts</h4>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>⚠️ Security Warning:</strong> This system logs attempted passwords for security analysis. Ensure database access is strictly controlled.
|
||||
</div>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>📌 IP Blocking Policy:</strong> Only failed attempts to the admin panel (<code>/@/*</code>) count toward IP blocking.
|
||||
API endpoint failures are logged but do NOT trigger automatic blocks.
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Alternating yellow shades for API rows */
|
||||
.table-warning-light {
|
||||
background-color: #fff9e6 !important; /* Lighter yellow */
|
||||
}
|
||||
|
||||
.table-warning {
|
||||
background-color: #fff3cd !important; /* Standard yellow */
|
||||
}
|
||||
|
||||
/* Tooltip container for positioning */
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Speech bubble tooltip */
|
||||
.speech-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #2c3e50;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Password tooltips: single line, auto-width */
|
||||
.pwd-btn + .speech-tooltip {
|
||||
white-space: nowrap;
|
||||
max-width: 90vw; /* Don't exceed viewport */
|
||||
}
|
||||
|
||||
/* User Agent tooltips: multi-line, 50% table width */
|
||||
.ua-btn + .speech-tooltip,
|
||||
.ua-btn .speech-tooltip {
|
||||
white-space: normal;
|
||||
max-width: 50%; /* 50% of container width */
|
||||
min-width: 200px;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Arrow pointing down */
|
||||
.speech-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Show tooltip when active */
|
||||
.speech-tooltip.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Clickable tooltip styling with cursor */
|
||||
.tooltip-btn {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-btn:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.tooltip-btn.active {
|
||||
background-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* User Agent button - make it look clickable */
|
||||
.ua-btn {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.ua-btn:hover {
|
||||
background-color: rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
/* Copy feedback animation */
|
||||
.copy-feedback {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
z-index: 2000;
|
||||
animation: fadeInOut 1.5s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% { opacity: 0; transform: translateX(-50%) translateY(5px); }
|
||||
20% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
80% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
100% { opacity: 0; transform: translateX(-50%) translateY(-5px); }
|
||||
}
|
||||
|
||||
/* Truncated text styling */
|
||||
.truncated {
|
||||
/* No underline - clean look */
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="table table-striped" style="font-size: 14px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>IP Address</th>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Path</th>
|
||||
<th>User Agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $index, $auth := .failedAuths}}
|
||||
{{$isAPI := not (hasPrefix $auth.Path "/@/")}}
|
||||
{{$rowClass := ""}}
|
||||
{{if $isAPI}}
|
||||
{{if eq (mod $index 2) 0}}
|
||||
{{$rowClass = "table-warning"}}
|
||||
{{else}}
|
||||
{{$rowClass = "table-warning-light"}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<tr class="{{$rowClass}}">
|
||||
<td>{{$auth.Timestamp.Format "01/02/2006 15:04:05"}}</td>
|
||||
<td><code>{{$auth.IPAddress}}</code></td>
|
||||
<td>{{if $auth.Username}}{{$auth.Username}}{{else}}<em>none</em>{{end}}</td>
|
||||
<td>
|
||||
{{if $auth.Password}}
|
||||
<div class="tooltip-container">
|
||||
<button class="btn btn-sm btn-outline-secondary tooltip-btn pwd-btn">👁</button>
|
||||
<div class="speech-tooltip">
|
||||
<code style="color: #ffc107; font-weight: bold;">{{$auth.Password}}</code>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<em>none</em>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<code>{{$auth.Path}}</code>
|
||||
{{if hasPrefix $auth.Path "/@/"}}
|
||||
<span class="badge badge-danger ml-1">Admin</span>
|
||||
{{else}}
|
||||
<span class="badge badge-secondary ml-1">API</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if $auth.UserAgent}}
|
||||
{{$ua := $auth.UserAgent}}
|
||||
{{if gt (len $ua) 10}}
|
||||
<div class="tooltip-container">
|
||||
<button class="tooltip-btn ua-btn" title="Click to copy">
|
||||
<span class="truncated">{{slice $ua 0 10}}...</span>
|
||||
</button>
|
||||
<div class="speech-tooltip">
|
||||
<span style="color: #fff;">{{$ua}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<span class="ua-btn" style="cursor: pointer;" title="Click to copy">{{$ua}}</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<em>none</em>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="/@/security/blocked-ips" class="btn btn-primary">View All Blocked IPs</a>
|
||||
<a href="/@/security/failed-auths" class="btn btn-secondary">View All Failed Attempts</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
console.log('Security dashboard JavaScript loaded');
|
||||
|
||||
// Helper function to copy text to clipboard
|
||||
function copyToClipboard(text, button) {
|
||||
// Modern clipboard API
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
showCopyFeedback(button, 'Copied!');
|
||||
}).catch(function(err) {
|
||||
console.error('Failed to copy:', err);
|
||||
fallbackCopy(text, button);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
fallbackCopy(text, button);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback copy method
|
||||
function fallbackCopy(text, button) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showCopyFeedback(button, 'Copied!');
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
showCopyFeedback(button, 'Copy failed');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Show copy feedback animation
|
||||
function showCopyFeedback(button, message) {
|
||||
var feedback = $('<div class="copy-feedback">' + message + '</div>');
|
||||
$(button).closest('.tooltip-container, td').append(feedback);
|
||||
setTimeout(function() {
|
||||
feedback.remove();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Unblock IP button handler
|
||||
$(document).on('click', '.unblock-btn', function(e) {
|
||||
e.preventDefault();
|
||||
var button = $(this);
|
||||
var ip = button.attr('data-ip');
|
||||
|
||||
console.log('Unblock button clicked for IP:', ip);
|
||||
|
||||
if (confirm('Are you sure you want to unblock IP: ' + ip + '?')) {
|
||||
$.ajax({
|
||||
url: '/@/security/unblock/' + encodeURIComponent(ip),
|
||||
type: 'POST',
|
||||
success: function(result) {
|
||||
console.log('Unblock success:', result);
|
||||
alert('IP ' + ip + ' has been unblocked successfully!');
|
||||
location.reload();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Unblock error:', xhr.responseText);
|
||||
alert('Error unblocking IP: ' + (xhr.responseText || error));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Password tooltips: Click to toggle AND copy
|
||||
$(document).on('click', '.pwd-btn', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var button = $(this);
|
||||
var tooltip = button.siblings('.speech-tooltip');
|
||||
var password = button.closest('.tooltip-container').find('.speech-tooltip code').text();
|
||||
|
||||
// Copy password to clipboard
|
||||
copyToClipboard(password, button);
|
||||
|
||||
// Close all other tooltips first
|
||||
$('.speech-tooltip').removeClass('active');
|
||||
$('.tooltip-btn').removeClass('active');
|
||||
|
||||
// Toggle this one
|
||||
if (tooltip.hasClass('active')) {
|
||||
tooltip.removeClass('active');
|
||||
button.removeClass('active');
|
||||
} else {
|
||||
tooltip.addClass('active');
|
||||
button.addClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
// User Agent tooltips: Hover to show, Click to copy
|
||||
$(document).on('mouseenter', '.ua-btn', function() {
|
||||
var tooltip = $(this).siblings('.speech-tooltip');
|
||||
tooltip.addClass('active');
|
||||
});
|
||||
|
||||
$(document).on('mouseleave', '.ua-btn', function() {
|
||||
var tooltip = $(this).siblings('.speech-tooltip');
|
||||
// Small delay to allow moving mouse to tooltip
|
||||
setTimeout(function() {
|
||||
if (!tooltip.is(':hover')) {
|
||||
tooltip.removeClass('active');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Click on User Agent to copy
|
||||
$(document).on('click', '.ua-btn', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var button = $(this);
|
||||
var userAgent = button.closest('.tooltip-container').find('.speech-tooltip span').text();
|
||||
|
||||
// If there's no tooltip (short UA), get text directly
|
||||
if (!userAgent) {
|
||||
userAgent = button.text().trim();
|
||||
}
|
||||
|
||||
copyToClipboard(userAgent, button);
|
||||
});
|
||||
|
||||
// Keep tooltip open if hovering over it
|
||||
$(document).on('mouseenter', '.ua-btn + .speech-tooltip', function() {
|
||||
$(this).addClass('active');
|
||||
});
|
||||
|
||||
$(document).on('mouseleave', '.ua-btn + .speech-tooltip', function() {
|
||||
$(this).removeClass('active');
|
||||
});
|
||||
|
||||
// Close password tooltips when clicking anywhere else on page
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.pwd-btn').length) {
|
||||
$('.pwd-btn').removeClass('active');
|
||||
$('.pwd-btn').siblings('.speech-tooltip').removeClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Event handlers attached. Found', $('.unblock-btn').length, 'unblock buttons');
|
||||
console.log('Event handlers attached. Found', $('.pwd-btn').length, 'password buttons');
|
||||
console.log('Event handlers attached. Found', $('.ua-btn').length, 'user agent buttons');
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user