pwpush update

This commit is contained in:
nahakubuilde
2025-07-18 06:47:38 +01:00
parent 36ea228663
commit 6a08d5af1c
6 changed files with 504 additions and 56 deletions
+1
View File
@@ -2,3 +2,4 @@
/go.sum /go.sum
/ImageMagick /ImageMagick
wordlist.txt wordlist.txt
*.db
BIN
View File
Binary file not shown.
+5 -2
View File
@@ -13,7 +13,10 @@ func (p *PWPusher) RegisterRoutes(mux *http.ServeMux) {
// API endpoint for checking link status // API endpoint for checking link status
mux.HandleFunc("/pwpush/api/status/", p.StatusHandler) mux.HandleFunc("/pwpush/api/status/", p.StatusHandler)
// PWPusher sub-routes (push viewing) // New short URL format for viewing
mux.HandleFunc("/s/", p.ViewHandler)
// PWPusher sub-routes (push viewing) - for backward compatibility
mux.HandleFunc("/pwpush/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/pwpush/", func(w http.ResponseWriter, r *http.Request) {
// Extract push ID from URL path like /pwpush/abc123 // Extract push ID from URL path like /pwpush/abc123
id := strings.TrimPrefix(r.URL.Path, "/pwpush/") id := strings.TrimPrefix(r.URL.Path, "/pwpush/")
@@ -26,7 +29,7 @@ func (p *PWPusher) RegisterRoutes(mux *http.ServeMux) {
} }
}) })
// Direct view handler for clean URLs // Direct view handler for clean URLs - for backward compatibility
mux.HandleFunc("/pwview/", p.ViewHandler) mux.HandleFunc("/pwview/", p.ViewHandler)
} }
+258 -53
View File
@@ -7,7 +7,6 @@ import (
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html" "html"
@@ -269,9 +268,33 @@ func loadViewTemplates(embeddedFS fs.FS) (*template.Template, error) {
} }
func (p *PWPusher) generateID() string { func (p *PWPusher) generateID() string {
bytes := make([]byte, 16) const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Generate random length between 10-16 characters
length := 10 + (int(p.randomByte()) % 7) // 10 + 0-6 = 10-16
bytes := make([]byte, length)
for i := range bytes {
bytes[i] = charset[p.randomByte()%byte(len(charset))]
}
return string(bytes)
}
func (p *PWPusher) generateEncryptionKey() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
// Generate random length between 5-10 characters (URL safe)
length := 5 + (int(p.randomByte()) % 6) // 5 + 0-5 = 5-10
bytes := make([]byte, length)
for i := range bytes {
bytes[i] = charset[p.randomByte()%byte(len(charset))]
}
return string(bytes)
}
func (p *PWPusher) randomByte() byte {
bytes := make([]byte, 1)
rand.Read(bytes) rand.Read(bytes)
return hex.EncodeToString(bytes) return bytes[0]
} }
func (p *PWPusher) encrypt(text string) (string, error) { func (p *PWPusher) encrypt(text string) (string, error) {
@@ -324,6 +347,71 @@ func (p *PWPusher) decrypt(encryptedText string) (string, error) {
return string(plaintext), nil return string(plaintext), nil
} }
// encryptWithKey encrypts text with an additional key layer
func (p *PWPusher) encryptWithKey(text, key string) (string, error) {
// First encrypt with the PWPusher's main encryption key
firstEncryption, err := p.encrypt(text)
if err != nil {
return "", err
}
// Create a hash of the additional key for AES
keyHash := sha256.Sum256([]byte(key))
block, err := aes.NewCipher(keyHash[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(firstEncryption), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decryptWithKey decrypts text that was encrypted with an additional key layer
func (p *PWPusher) decryptWithKey(encryptedText, key string) (string, error) {
data, err := base64.StdEncoding.DecodeString(encryptedText)
if err != nil {
return "", err
}
// Create a hash of the additional key for AES
keyHash := sha256.Sum256([]byte(key))
block, err := aes.NewCipher(keyHash[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
firstDecryption, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
// Now decrypt with the PWPusher's main encryption key
return p.decrypt(string(firstDecryption))
}
// Rate limiting methods for password attempts // Rate limiting methods for password attempts
func (p *PWPusher) isBlocked(clientIP string) bool { func (p *PWPusher) isBlocked(clientIP string) bool {
if attempts, exists := p.failedAttempts[clientIP]; exists { if attempts, exists := p.failedAttempts[clientIP]; exists {
@@ -508,8 +596,11 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
return return
} }
// Encrypt text // Generate additional encryption key
encryptedText, err := p.encrypt(req.Text) additionalKey := p.generateEncryptionKey()
// Encrypt text with double encryption
encryptedText, err := p.encryptWithKey(req.Text, additionalKey)
if err != nil { if err != nil {
log.Printf("Encryption error: %v", err) log.Printf("Encryption error: %v", err)
http.Error(w, "Failed to encrypt text", http.StatusInternalServerError) http.Error(w, "Failed to encrypt text", http.StatusInternalServerError)
@@ -547,12 +638,12 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
return return
} }
// Prepare response URL // Prepare response URL with new format
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
scheme = "https" scheme = "https"
} }
fullURL := fmt.Sprintf("%s://%s/pwview/%s", scheme, r.Host, id) fullURL := fmt.Sprintf("%s://%s/s/%s?k=%s", scheme, r.Host, id, additionalKey)
// Save to user's history if tracking is enabled // Save to user's history if tracking is enabled
if req.TrackHistory { if req.TrackHistory {
@@ -583,21 +674,47 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) { func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL path // Extract ID from URL path
var path string var id string
if strings.HasPrefix(r.URL.Path, "/pwview/") { var encryptionKey string
path = strings.TrimPrefix(r.URL.Path, "/pwview/")
} else if strings.HasPrefix(r.URL.Path, "/pwpush/view/") {
path = strings.TrimPrefix(r.URL.Path, "/pwpush/view/")
} else {
path = strings.TrimPrefix(r.URL.Path, "/pwpush/")
}
if path == "" { if strings.HasPrefix(r.URL.Path, "/s/") {
http.Error(w, "Invalid push ID", http.StatusBadRequest) // New format: /s/<id>?k=<encryption_key>
id = strings.TrimPrefix(r.URL.Path, "/s/")
encryptionKey = r.URL.Query().Get("k")
if encryptionKey == "" {
// Redirect to PWPusher main page with error popup
errorMsg := url.QueryEscape("Missing encryption key. Please use the complete secure link.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
}
} else if strings.HasPrefix(r.URL.Path, "/pwview/") {
// Legacy format: /pwview/<id>
// For legacy URLs, we'll show an error since they don't have the encryption key
errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
} else if strings.HasPrefix(r.URL.Path, "/pwpush/view/") {
// Legacy format: /pwpush/view/<id>
// For legacy URLs, we'll show an error since they don't have the encryption key
errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
} else {
// Legacy format: /pwpush/<id>
// For legacy URLs, we'll show an error since they don't have the encryption key
errorMsg := url.QueryEscape("This link format is no longer supported. Please use the new secure link format.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return return
} }
log.Printf("ViewHandler: Extracted ID '%s' from URL '%s'", path, r.URL.Path) if id == "" {
errorMsg := url.QueryEscape("Invalid push ID.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
}
log.Printf("ViewHandler: Extracted ID '%s' from URL '%s'", id, r.URL.Path)
// Handle POST requests (reveal actions and password verification) // Handle POST requests (reveal actions and password verification)
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
@@ -606,15 +723,74 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
// Validate CSRF token for form submissions // Validate CSRF token for form submissions
csrfToken := r.FormValue("csrf_token") csrfToken := r.FormValue("csrf_token")
if !p.validateCSRFToken(csrfToken) { if !p.validateCSRFToken(csrfToken) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden) // Redirect with error popup instead of showing empty error page
errorMsg := url.QueryEscape("Invalid or expired security token. Please try again.")
redirectURL := fmt.Sprintf("%s?k=%s&error=%s", r.URL.Path, encryptionKey, errorMsg)
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return return
} }
action := r.FormValue("action") action := r.FormValue("action")
if action == "reveal" { if action == "reveal" {
// Redirect to same URL with show=true parameter // Handle reveal action - decrypt and return content directly
http.Redirect(w, r, r.URL.Path+"?show=true", http.StatusSeeOther) // Get push data
push, err := p.getPushByID(id)
if err != nil {
// Redirect with error popup instead of empty page
errorMsg := url.QueryEscape("Link not found or has been deleted.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
}
// Check if expired
if time.Now().After(push.ExpiresAt) || push.IsDeleted {
p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush",
Error: "This link has expired or been deleted.",
})
return
}
// Check if max views reached
if push.CurrentViews >= push.MaxViews {
p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush",
Error: "This link has reached its maximum view limit.",
})
return
}
// Increment view count
_, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id)
if err != nil {
log.Printf("Failed to increment view count: %v", err)
}
// Refresh push data to get updated view count
push, err = p.getPushByID(id)
if err != nil {
log.Printf("Failed to get updated push data: %v", err)
}
// Decrypt text with the encryption key
text, err := p.decryptWithKey(push.EncryptedText, encryptionKey)
if err != nil {
log.Printf("Failed to decrypt text: %v", err)
// Redirect with error popup instead of empty page
errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
}
// Render the text with delete option if auto-delete is enabled
p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush",
Push: push,
Text: text,
ShowText: true,
Revealed: true,
})
return return
} else if action == "verify_password" { } else if action == "verify_password" {
// Handle password verification with rate limiting // Handle password verification with rate limiting
@@ -627,7 +803,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("IP %s is blocked until %v", clientIP, blockedUntil) log.Printf("IP %s is blocked until %v", clientIP, blockedUntil)
p.renderTemplate(w, "pwview.html", ViewData{ p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush", CurrentPage: "pwpush",
Push: &PushData{ID: path}, // Minimal push data for display Push: &PushData{ID: id}, // Minimal push data for display
RequirePassword: true, RequirePassword: true,
IsBlocked: true, IsBlocked: true,
BlockedUntil: blockedUntil, BlockedUntil: blockedUntil,
@@ -639,9 +815,11 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
password := r.FormValue("password") password := r.FormValue("password")
// Get push data to check password // Get push data to check password
push, err := p.getPushByID(path) push, err := p.getPushByID(id)
if err != nil { if err != nil {
http.Error(w, "Link not found", http.StatusNotFound) // Redirect with error popup instead of empty page
errorMsg := url.QueryEscape("Link not found or has been deleted.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return return
} }
@@ -675,14 +853,44 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// Password is correct - reset failed attempts and redirect // Password is correct - reset failed attempts and show content directly
log.Printf("Correct password for IP %s, resetting attempts", clientIP) log.Printf("Correct password for IP %s, resetting attempts", clientIP)
p.resetFailedAttempts(clientIP) p.resetFailedAttempts(clientIP)
http.Redirect(w, r, r.URL.Path+"?show=true&verified=true", http.StatusSeeOther)
// Decrypt text with the encryption key
text, err := p.decryptWithKey(push.EncryptedText, encryptionKey)
if err != nil {
log.Printf("Failed to decrypt text: %v", err)
// Redirect with error popup instead of empty page
errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return
}
// Increment view count since password is correct
_, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id)
if err != nil {
log.Printf("Failed to increment view count: %v", err)
}
// Refresh push data to get updated view count
push, err = p.getPushByID(id)
if err != nil {
log.Printf("Failed to get updated push data: %v", err)
}
// Render the text directly (no click required since password was verified)
p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush",
Push: push,
Text: text,
ShowText: true,
Revealed: true,
})
return return
} else if action == "delete" { } else if action == "delete" {
// Manual delete action // Manual delete action
_, err := p.db.Exec("UPDATE pushes SET is_deleted = 1 WHERE id = ?", path) _, err := p.db.Exec("UPDATE pushes SET is_deleted = 1 WHERE id = ?", id)
if err != nil { if err != nil {
log.Printf("Failed to mark as deleted: %v", err) log.Printf("Failed to mark as deleted: %v", err)
} }
@@ -694,11 +902,8 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// Check if this is a reveal request
showText := r.URL.Query().Get("show") == "true"
// Get push data // Get push data
push, err := p.getPushByID(path) push, err := p.getPushByID(id)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
p.renderTemplate(w, "pwview.html", ViewData{ p.renderTemplate(w, "pwview.html", ViewData{
@@ -731,8 +936,7 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
} }
// Check if password is required and not yet verified // Check if password is required and not yet verified
passwordVerified := r.URL.Query().Get("verified") == "true" if push.PasswordHash != "" {
if push.PasswordHash != "" && !passwordVerified {
clientIP := p.getClientIP(r) clientIP := p.getClientIP(r)
// Check if client is blocked // Check if client is blocked
@@ -760,8 +964,8 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// If require click is enabled and showText is not true, show the reveal page // If require click is enabled, show the reveal page (only for GET requests)
if push.RequireClick && !showText && push.PasswordHash == "" { if push.RequireClick {
p.renderTemplate(w, "pwview.html", ViewData{ p.renderTemplate(w, "pwview.html", ViewData{
CurrentPage: "pwpush", CurrentPage: "pwpush",
Push: push, Push: push,
@@ -771,25 +975,26 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Increment view count only when actually viewing content // If no restrictions (no password, no click required), show content directly
if showText || !push.RequireClick { // Increment view count
_, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", path) _, err = p.db.Exec("UPDATE pushes SET current_views = current_views + 1 WHERE id = ?", id)
if err != nil { if err != nil {
log.Printf("Failed to increment view count: %v", err) log.Printf("Failed to increment view count: %v", err)
}
// Refresh push data to get updated view count
push, err = p.getPushByID(path)
if err != nil {
log.Printf("Failed to get updated push data: %v", err)
}
} }
// Decrypt text // Refresh push data to get updated view count
text, err := p.decrypt(push.EncryptedText) push, err = p.getPushByID(id)
if err != nil {
log.Printf("Failed to get updated push data: %v", err)
}
// Decrypt text with the encryption key
text, err := p.decryptWithKey(push.EncryptedText, encryptionKey)
if err != nil { if err != nil {
log.Printf("Failed to decrypt text: %v", err) log.Printf("Failed to decrypt text: %v", err)
http.Error(w, "Failed to decrypt content", http.StatusInternalServerError) // Redirect with error popup instead of empty page
errorMsg := url.QueryEscape("Failed to decrypt content - invalid encryption key. Please check your link.")
http.Redirect(w, r, fmt.Sprintf("/pwpush?error=%s", errorMsg), http.StatusSeeOther)
return return
} }
@@ -898,9 +1103,9 @@ func (p *PWPusher) validateAndSanitizeText(text string) (string, error) {
return "", fmt.Errorf("text contains invalid UTF-8 characters") return "", fmt.Errorf("text contains invalid UTF-8 characters")
} }
// Sanitize HTML // Store the original text without HTML escaping
sanitized := html.EscapeString(text) // The template will handle safe display
return sanitized, nil return text, nil
} }
func (p *PWPusher) validatePassword(password string) error { func (p *PWPusher) validatePassword(password string) error {
+121
View File
@@ -6,6 +6,61 @@
<title>{{block "title" .}}HeaderAnalyzer{{end}}</title> <title>{{block "title" .}}HeaderAnalyzer{{end}}</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="icon" href="/favicon.ico" type="image/x-icon"> <link rel="icon" href="/favicon.ico" type="image/x-icon">
<style>
/* Popup notification styles */
.popup-notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: #f44336;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 400px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
}
.popup-notification.show {
transform: translateX(0);
}
.popup-notification.error {
background: #f44336;
}
.popup-notification.warning {
background: #ff9800;
}
.popup-notification.success {
background: #4caf50;
}
.popup-notification.info {
background: #2196f3;
}
.popup-notification .close-btn {
float: right;
background: none;
border: none;
color: white;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-left: 10px;
padding: 0;
line-height: 1;
}
.popup-notification .close-btn:hover {
opacity: 0.7;
}
</style>
{{block "head" .}}{{end}} {{block "head" .}}{{end}}
</head> </head>
<body> <body>
@@ -25,6 +80,72 @@
{{end}} {{end}}
</main> </main>
<!-- Popup notification container -->
<div id="popup-container"></div>
<script>
// Popup notification system
function showPopup(message, type = 'error', duration = 5000) {
const container = document.getElementById('popup-container');
const popup = document.createElement('div');
popup.className = `popup-notification ${type}`;
popup.innerHTML = `
<button class="close-btn" onclick="this.parentElement.remove()">&times;</button>
${message}
`;
container.appendChild(popup);
// Trigger animation
setTimeout(() => popup.classList.add('show'), 10);
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
popup.classList.remove('show');
setTimeout(() => popup.remove(), 300);
}, duration);
}
}
// Show popup from URL parameters (for redirects)
function checkForErrorParams() {
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
const warning = urlParams.get('warning');
const success = urlParams.get('success');
const info = urlParams.get('info');
if (error) {
showPopup(decodeURIComponent(error), 'error');
// Clean URL
urlParams.delete('error');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (warning) {
showPopup(decodeURIComponent(warning), 'warning');
urlParams.delete('warning');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (success) {
showPopup(decodeURIComponent(success), 'success');
urlParams.delete('success');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (info) {
showPopup(decodeURIComponent(info), 'info');
urlParams.delete('info');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
}
// Check for error parameters when page loads
document.addEventListener('DOMContentLoaded', checkForErrorParams);
</script>
{{block "scripts" .}}{{end}} {{block "scripts" .}}{{end}}
</body> </body>
</html> </html>
+118
View File
@@ -300,6 +300,60 @@
box-shadow: 0 0 0 3px rgba(60, 92, 124, 0.3); box-shadow: 0 0 0 3px rgba(60, 92, 124, 0.3);
outline: none; outline: none;
} }
/* Popup notification styles */
.popup-notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: #f44336;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 400px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
}
.popup-notification.show {
transform: translateX(0);
}
.popup-notification.error {
background: #f44336;
}
.popup-notification.warning {
background: #ff9800;
}
.popup-notification.success {
background: #4caf50;
}
.popup-notification.info {
background: #2196f3;
}
.popup-notification .close-btn {
float: right;
background: none;
border: none;
color: white;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-left: 10px;
padding: 0;
line-height: 1;
}
.popup-notification .close-btn:hover {
opacity: 0.7;
}
</style> </style>
</head> </head>
<body> <body>
@@ -496,8 +550,72 @@ document.addEventListener('DOMContentLoaded', function() {
// Auto-select short content // Auto-select short content
setTimeout(() => selectAll(), 500); setTimeout(() => selectAll(), 500);
} }
// Check for error parameters when page loads
checkForErrorParams();
}); });
// Popup notification system
function showPopup(message, type = 'error', duration = 5000) {
const container = document.getElementById('popup-container');
const popup = document.createElement('div');
popup.className = `popup-notification ${type}`;
popup.innerHTML = `
<button class="close-btn" onclick="this.parentElement.remove()">&times;</button>
${message}
`;
container.appendChild(popup);
// Trigger animation
setTimeout(() => popup.classList.add('show'), 10);
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
popup.classList.remove('show');
setTimeout(() => popup.remove(), 300);
}, duration);
}
}
// Show popup from URL parameters (for redirects)
function checkForErrorParams() {
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
const warning = urlParams.get('warning');
const success = urlParams.get('success');
const info = urlParams.get('info');
if (error) {
showPopup(decodeURIComponent(error), 'error');
// Clean URL
urlParams.delete('error');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (warning) {
showPopup(decodeURIComponent(warning), 'warning');
urlParams.delete('warning');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (success) {
showPopup(decodeURIComponent(success), 'success');
urlParams.delete('success');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
if (info) {
showPopup(decodeURIComponent(info), 'info');
urlParams.delete('info');
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
}
}
</script> </script>
<!-- Popup notification container -->
<div id="popup-container"></div>
</body> </body>
</html> </html>