fix layout, add correct protocol for pwpusher as well as message for sender.

This commit is contained in:
nahakubuilde
2025-07-22 07:28:30 +01:00
parent 173569365b
commit fd8d20f642
29 changed files with 2989 additions and 1175 deletions

5
.gitignore vendored
View File

@@ -2,4 +2,7 @@
/go.sum
/ImageMagick
wordlist.txt
*.db
*.db
/app_build/README.md
/app_build/tailwindcss-linux-x64

View File

@@ -1,17 +1,21 @@
# Email Header Analyzer - Build & Usage Instructions
# GoNetKit
- Go Web App where you can find many usefull IT tools
- These tools should provide simple and nice way for user to get what they need without requiring signing up or sharing details with 3rd parties.
## Building a Single-File Executable (Windows & Linux)
This app uses Go's `embed` package to bundle all static files (web UI, icons, CSS, etc.) into a single executable. You do **not** need to distribute the `web/` folder separately.
- You will need C building package, especially on Windows as this app using SQLITE, if you do not need password pushser, then remove it and you will not need it to make windows built.
- built scripts are in `app_build` folder, but may need fixing as the app is in development still and I built it on few computers when i have time.
### Prerequisites
- [Go 1.16+](https://golang.org/dl/) (required for `embed`)
- [Go 1.24+](https://golang.org/dl/) (required for `embed`)
- (Optional) [Python 3](https://www.python.org/) with Pillow for icon conversion
```go
go mod init headeranalyzer
go mod init gonetkit
go mod tidy
```
### 1. Prepare Icons
### 1. Prepare Icons (custom icon Optional)
- Place your tray icon and favicon in `web/icon.png` and `web/favicon.png`.
- For Windows tray icon, you **must** use a `.ico` file. Use the provided script:
@@ -20,6 +24,7 @@ go mod tidy
python web/convert_icon.py web/icon.png web/icon.ico
# using ImageMagick convertor
magick envelope.jpg -define icon:auto-resize=16,32,48,64 favicon.ico
# https://github.com/ImageMagick/ImageMagick/releases
ImageMagick\magick.exe web\icon.png -define icon:auto-resize=16,32,48,64 web\favicon.ico
ImageMagick\magick.exe identify web\favicon.ico
# App Icon:
@@ -37,19 +42,19 @@ go clean -cache
#### Windows (from Windows PowerShell or Command Prompt):
```powershell
# PowerShell (set environment variables before the command)
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o GoNetKit.exe main.go
```
```cmd
REM Command Prompt (set environment variables before the command)
set GOOS=windows
set GOARCH=amd64
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
go build -ldflags "-H=windowsgui" -o GoNetKit.exe main.go
```
#### Linux/macOS (from Bash):
```sh
# Build 64-bit Linux executable
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
GOOS=linux GOARCH=amd64 go build -o goNetKit main.go
```
- The resulting executable contains all static files and icons.
@@ -74,13 +79,13 @@ By default, Go does not embed an icon in the .exe. To add your tray/web icon as
#### Windows (from Windows PowerShell or Command Prompt):
```powershell
# PowerShell (set environment variables before the command)
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o GoNetKit.exe main.go
```
```cmd
REM Command Prompt (set environment variables before the command)
set GOOS=windows
set GOARCH=amd64
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
go build -ldflags "-H=windowsgui" -o GoNetKit.exe main.go
```
- The `-ldflags "-H=windowsgui"` flag prevents a console window from opening when you run the app.
@@ -89,7 +94,7 @@ go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
#### Linux/macOS (from Bash):
```sh
# Build 64-bit Linux executable
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
GOOS=linux GOARCH=amd64 go build -o goNetKit main.go
```
- The resulting executable contains all static files and icons.
@@ -97,9 +102,9 @@ GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
### 3. Run the App
- Double-click or run from terminal:
- On Windows: `headeranalyzer.exe`
- On Linux: `./headeranalyzer`
- The app will start a web server (default: http://localhost:8080) and show a system tray icon.
- On Windows: `GoNetKit.exe`
- On Linux: `./goNetKit`
- The app will start a web server (default: http://localhost:5555) and show a system tray icon.
- Use the tray menu to open the web UI or quit the app.
### 4. Usage Notes
@@ -123,8 +128,8 @@ If you followed all steps and the icon still does not appear:
- **Try building without cross-compiling:** If you are cross-compiling, try building directly on Windows.
- **Try go build without -ldflags:** Rarely, the `-ldflags` flag can interfere. Try building with and without it:
```powershell
go build -o headeranalyzer.exe main.go
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
go build -o GoNetKit.exe main.go
go build -ldflags "-H=windowsgui" -o GoNetKit.exe main.go
```
- **Try go generate:** If you use `go generate`, ensure it does not overwrite or remove `icon.syso`.

View File

@@ -0,0 +1,71 @@
@echo off
REM Tailwind CSS Build Script for Header Analyzer (Windows)
REM This script downloads Tailwind CLI and generates CSS for production
setlocal enabledelayedexpansion
set "PROJECT_ROOT=%~dp0.."
set "TAILWIND_CLI=%PROJECT_ROOT%\app_build\tailwindcss-windows-x64.exe"
set "INPUT_CSS=%PROJECT_ROOT%\web\style-custom.css"
set "OUTPUT_CSS=%PROJECT_ROOT%\web\style-compiled.css"
set "CONFIG_FILE=%PROJECT_ROOT%\tailwind.config.js"
echo 🎨 Building Tailwind CSS for Header Analyzer...
echo Project root: %PROJECT_ROOT%
REM Download Tailwind CLI if it doesn't exist
if not exist "%TAILWIND_CLI%" (
echo 📥 Downloading Tailwind CSS CLI...
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-windows-x64.exe
move tailwindcss-windows-x64.exe "%TAILWIND_CLI%"
echo ✅ Tailwind CLI downloaded
)
REM Create tailwind.config.js if it doesn't exist
if not exist "%CONFIG_FILE%" (
echo ⚙️ Creating tailwind.config.js...
(
echo /** @type {import('tailwindcss'^).Config} */
echo module.exports = {
echo content: [
echo "./web/**/*.html",
echo "./web/**/*.js"
echo ],
echo theme: {
echo extend: {
echo colors: {
echo 'header-blue': '#007cba',
echo 'header-dark': '#1a1a1a'
echo }
echo },
echo },
echo plugins: [],
echo }
) > "%CONFIG_FILE%"
echo ✅ Created tailwind.config.js
)
REM Build CSS
echo 🔨 Building Tailwind CSS...
if "%1"=="--watch" (
echo 👀 Starting watch mode...
"%TAILWIND_CLI%" -i "%INPUT_CSS%" -o "%OUTPUT_CSS%" --watch
) else if "%1"=="--dev" (
echo 🛠️ Building development CSS...
"%TAILWIND_CLI%" -i "%INPUT_CSS%" -o "%OUTPUT_CSS%"
) else (
echo 📦 Building production CSS ^(minified^)...
"%TAILWIND_CLI%" -i "%INPUT_CSS%" -o "%OUTPUT_CSS%" --minify
)
echo ✅ Tailwind CSS build complete!
echo 📍 Output: %OUTPUT_CSS%
REM Show file size
if exist "%OUTPUT_CSS%" (
for %%F in ("%OUTPUT_CSS%") do (
echo 📊 Generated CSS size: %%~zF bytes
)
)
pause

70
app_build/build-tailwind.sh Executable file
View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Tailwind CSS Build Script for Header Analyzer
# This script downloads Tailwind CLI and generates CSS for production
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TAILWIND_CLI="$PROJECT_ROOT/app_build/tailwindcss-linux-x64"
INPUT_CSS="$PROJECT_ROOT/web/style-input.css"
OUTPUT_CSS="$PROJECT_ROOT/web/style-tailwind.css"
CONFIG_FILE="$PROJECT_ROOT/tailwind.config.js"
echo "🎨 Building Tailwind CSS for Header Analyzer..."
echo "Project root: $PROJECT_ROOT"
# Download Tailwind CLI if it doesn't exist
if [ ! -f "$TAILWIND_CLI" ]; then
echo "📥 Downloading Tailwind CSS CLI..."
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
mv tailwindcss-linux-x64 "$TAILWIND_CLI"
chmod +x "$TAILWIND_CLI"
echo "✅ Tailwind CLI downloaded"
fi
# Create tailwind.config.js if it doesn't exist
if [ ! -f "$CONFIG_FILE" ]; then
echo "⚙️ Creating tailwind.config.js..."
cat > "$CONFIG_FILE" << 'EOF'
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./web/**/*.html",
"./web/**/*.js"
],
theme: {
extend: {
colors: {
'header-blue': '#007cba',
'header-dark': '#1a1a1a'
}
},
},
plugins: [],
}
EOF
echo "✅ Created tailwind.config.js"
fi
# Build CSS
echo "🔨 Building Tailwind CSS..."
if [ "$1" = "--watch" ]; then
echo "👀 Starting watch mode..."
"$TAILWIND_CLI" -i "$INPUT_CSS" -o "$OUTPUT_CSS" --watch
elif [ "$1" = "--dev" ]; then
echo "🛠️ Building development CSS..."
"$TAILWIND_CLI" -i "$INPUT_CSS" -o "$OUTPUT_CSS"
else
echo "📦 Building production CSS (minified)..."
"$TAILWIND_CLI" -i "$INPUT_CSS" -o "$OUTPUT_CSS" --minify
fi
echo "✅ Tailwind CSS build complete!"
echo "📍 Output: $OUTPUT_CSS"
# Show file size
if [ -f "$OUTPUT_CSS" ]; then
SIZE=$(du -h "$OUTPUT_CSS" | cut -f1)
echo "📊 Generated CSS size: $SIZE"
fi

43
app_build/build.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
# Main Build Script for Header Analyzer
# Builds Tailwind CSS and compiles the Go application
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="$PROJECT_ROOT/app_build"
echo "🚀 Building Header Analyzer..."
echo "Project root: $PROJECT_ROOT"
# Build Tailwind CSS first
echo "📝 Step 1: Building Tailwind CSS..."
"$BUILD_DIR/build-tailwind.sh"
# Build Go application
echo "📝 Step 2: Building Go application..."
cd "$PROJECT_ROOT"
# Set build variables
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo "Version: $VERSION"
echo "Build time: $BUILD_TIME"
echo "Commit: $COMMIT"
# Build for current platform
echo "🔨 Compiling for current platform..."
go build -ldflags "-X main.version=$VERSION -X main.buildTime=$BUILD_TIME -X main.commit=$COMMIT" \
-o headeranalyzer .
echo "✅ Build complete!"
echo "📍 Binary: $PROJECT_ROOT/headeranalyzer"
# Show binary size
if [ -f "$PROJECT_ROOT/headeranalyzer" ]; then
SIZE=$(du -h "$PROJECT_ROOT/headeranalyzer" | cut -f1)
echo "📊 Binary size: $SIZE"
fi

View File

@@ -1,6 +1,3 @@
/* Import Tailwind CSS */
@import url('https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css');
:root {
--bg-color: #0f172a;
--text-color: #e2e8f0;
@@ -22,6 +19,34 @@
.text-dark-muted { color: var(--dark-muted); }
.border-dark-border { border-color: var(--dark-border); }
/* Custom animations and styles */
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}
.animate-slide-out-right {
animation: slideOutRight 0.3s ease-in forwards;
}
/* Backdrop blur for popup */
.backdrop-blur-popup {
backdrop-filter: blur(8px);
}
/* Floating button styles */
.floating-nav {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Password Generator Styles */
.password-generator {
max-width: 56rem;

21
main.go
View File

@@ -16,11 +16,11 @@ import (
"syscall"
"time"
"headeranalyzer/landingpage"
"headeranalyzer/parser"
"headeranalyzer/passwordgenerator"
"headeranalyzer/pwpusher"
"headeranalyzer/resolver"
"gonetkit/landingpage"
"gonetkit/parser"
"gonetkit/passwordgenerator"
"gonetkit/pwpusher"
"gonetkit/resolver"
"github.com/getlantern/systray"
)
@@ -174,6 +174,17 @@ func main() {
w.Write(data)
})
// Serve Tailwind CSS file with correct MIME type
http.HandleFunc("/style-tailwind.css", func(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(staticFS, "style-tailwind.css")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/css")
w.Write(data)
})
http.Handle("/", landingHandler)
http.Handle("/analyze", indexHandler)

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"headeranalyzer/resolver"
"gonetkit/resolver"
)
type Report struct {

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"headeranalyzer/security"
"gonetkit/security"
)
type Handler struct {
@@ -73,6 +73,44 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Validate CSRF token
csrfToken := r.FormValue("csrf_token")
if !h.csrf.ValidateToken(csrfToken) {
// If CSRF validation fails, check if this looks like a refresh attempt
// by seeing if we have headers data
headers := r.FormValue("headers")
if headers != "" {
// This appears to be a refresh attempt with stale CSRF token
// Generate a new token and re-analyze
log.Printf("DEBUG: CSRF validation failed, but reanalyzing with fresh token for refresh")
freshCSRFToken, err := h.csrf.GenerateToken()
if err != nil {
http.Redirect(w, r, "/?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
return
}
validatedHeaders, err := h.validator.ValidateEmailHeaders(headers)
if err != nil {
log.Printf("DEBUG: Validation failed on refresh: %v", err)
http.Redirect(w, r, "/?error="+url.QueryEscape("Invalid input provided"), http.StatusSeeOther)
return
}
report := Analyze(validatedHeaders)
data := struct {
*Report
CurrentPage string
CSRFToken string
OriginalHeaders string
}{
Report: report,
CurrentPage: "home",
CSRFToken: freshCSRFToken,
OriginalHeaders: validatedHeaders,
}
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
return
}
// Regular CSRF failure - redirect to home
http.Redirect(w, r, "/?error="+url.QueryEscape("Invalid security token. Please try again."), http.StatusSeeOther)
return
}
@@ -95,13 +133,26 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("DEBUG: Headers validated successfully")
report := Analyze(validatedHeaders)
log.Printf("DEBUG: Analysis completed, From field: '%s'", report.From)
// Create a wrapper struct to include current page info
// Generate a fresh CSRF token for the result page
freshCSRFToken, err := h.csrf.GenerateToken()
if err != nil {
log.Printf("ERROR: Failed to generate fresh CSRF token: %v", err)
http.Redirect(w, r, "/?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
return
}
// Create a wrapper struct to include current page info and fresh CSRF token
data := struct {
*Report
CurrentPage string
CurrentPage string
CSRFToken string
OriginalHeaders string
}{
Report: report,
CurrentPage: "home",
Report: report,
CurrentPage: "home",
CSRFToken: freshCSRFToken,
OriginalHeaders: validatedHeaders,
}
log.Printf("DEBUG: About to render template with data")

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
"headeranalyzer/security"
"gonetkit/security"
)
var validator = security.NewInputValidator()

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"headeranalyzer/security"
"gonetkit/security"
)
type Handler struct {

View File

@@ -20,7 +20,7 @@ import (
"strings"
"time"
"headeranalyzer/security"
"gonetkit/security"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
@@ -605,9 +605,18 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
// Prepare response URL with new format
scheme := "http"
// Check for HTTPS in multiple ways
if r.TLS != nil {
scheme = "https"
} else if r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
} else if r.Header.Get("X-Forwarded-Ssl") == "on" {
scheme = "https"
} else if strings.HasPrefix(r.Header.Get("Referer"), "https://") {
scheme = "https"
}
fullURL := fmt.Sprintf("%s://%s/s/%s?k=%s", scheme, r.Host, id, additionalKey)
// Save to user's history if tracking is enabled
@@ -632,6 +641,7 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
"PushURL": fullURL,
"ID": id,
"ExpiresAt": expiresAt.Format("2006-01-02 15:04:05"),
"MaxViews": req.MaxViews,
"CurrentPage": "pwpush",
})
}

View File

@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
"headeranalyzer/security"
"gonetkit/security"
"github.com/miekg/dns"
)

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"headeranalyzer/security"
"gonetkit/security"
)
type Handler struct {

9
tailwind.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./web/*.{html,js}",
"./**/*.go"
],
safelist: [],
plugins: [],
}

View File

@@ -4,62 +4,35 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}{{end}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#1e1e1e',
surface: '#2d2d2d',
border: '#404040',
text: '#e0e0e0',
muted: '#999999'
}
}
}
}
}
</script>
<link rel="stylesheet" href="/style-tailwind.css">
<style>
/* Custom animations and styles */
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
/* Prevent horizontal overflow */
* {
box-sizing: border-box;
}
html, body {
overflow-x: hidden;
max-width: 100vw;
width: 100%;
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
/* Ensure proper container widths */
.container {
max-width: 100vw;
width: 100%;
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}
.animate-slide-out-right {
animation: slideOutRight 0.3s ease-in forwards;
}
/* Backdrop blur for popup */
.backdrop-blur-popup {
backdrop-filter: blur(8px);
}
/* Floating button styles */
.floating-nav {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
main {
max-width: 100vw;
width: 100%;
}
</style>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
{{block "head" .}}{{end}}
</head>
<body class="bg-dark-bg text-dark-text min-h-screen">
<!-- Floating Navigation -->
<div class="fixed top-4 right-4 z-50 floating-nav">
<div class="fixed top-4 right-4 z-50 floating-nav rounded-full">
<div class="flex bg-dark-surface border border-dark-border rounded-full overflow-hidden">
<!-- Home Button -->
<a href="/" class="flex items-center justify-center w-12 h-12 bg-blue-600 hover:bg-blue-700 transition-colors duration-200 text-white">
@@ -78,27 +51,27 @@
</div>
<!-- Navigation Popup -->
<div id="navigationPopup" class="fixed inset-0 z-40 hidden">
<div id="navigationPopup" class="fixed inset-0 z-40 flex items-center justify-center p-4 transition-opacity duration-300 opacity-0 pointer-events-none overflow-y-auto">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-popup" onclick="closeNavigationPopup()"></div>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeNavigationPopup()"></div>
<!-- Popup Content -->
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-dark-surface border border-dark-border rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-dark-text">Navigation</h2>
<button onclick="closeNavigationPopup()" class="text-dark-muted hover:text-dark-text transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="relative bg-dark-surface border border-dark-border rounded-xl p-6 w-full max-w-lg shadow-2xl z-10 transition-transform duration-300 scale-95 my-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-dark-text">Navigation</h2>
<button onclick="closeNavigationPopup()" class="p-1 text-dark-muted hover:text-dark-text hover:bg-dark-bg rounded-lg transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- IT Tools Category -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-blue-400 uppercase tracking-wide mb-3">IT Tools</h3>
<div class="space-y-2">
<a href="/analyze" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
<div class="flex items-center justify-center w-10 h-10 bg-green-600 rounded-lg mr-3 group-hover:bg-green-700 transition-colors">
<div class="flex items-center justify-center w-10 h-10 bg-green-600 rounded-lg mr-4 group-hover:bg-green-700 transition-colors">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
@@ -111,7 +84,7 @@
</a>
<a href="/dns" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
<div class="flex items-center justify-center w-10 h-10 bg-purple-600 rounded-lg mr-3 group-hover:bg-purple-700 transition-colors">
<div class="flex items-center justify-center w-10 h-10 bg-purple-600 rounded-lg mr-4 group-hover:bg-purple-700 transition-colors">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
@@ -126,10 +99,10 @@
<!-- Online Security Category -->
<div>
<h3 class="text-sm font-semibold text-orange-400 uppercase tracking-wide mb-3">Online Security</h3>
<h3 class="text-sm font-semibold text-blue-400 uppercase tracking-wide mb-3">Online Security</h3>
<div class="space-y-2">
<a href="/pwgenerator" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
<div class="flex items-center justify-center w-10 h-10 bg-yellow-600 rounded-lg mr-3 group-hover:bg-yellow-700 transition-colors">
<div class="flex items-center justify-center w-10 h-10 bg-yellow-600 rounded-lg mr-4 group-hover:bg-yellow-700 transition-colors">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-0.257-.257A6 6 0 1118 8zm-1.5 0a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM10 7a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd"/>
</svg>
@@ -141,7 +114,7 @@
</a>
<a href="/pwpush" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
<div class="flex items-center justify-center w-10 h-10 bg-red-600 rounded-lg mr-3 group-hover:bg-red-700 transition-colors">
<div class="flex items-center justify-center w-10 h-10 bg-red-600 rounded-lg mr-4 group-hover:bg-red-700 transition-colors">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
</svg>
@@ -172,18 +145,63 @@
// Navigation popup functionality
function openNavigationPopup() {
const popup = document.getElementById('navigationPopup');
popup.classList.remove('hidden');
const content = popup.querySelector('.relative');
// Prevent body scrolling
document.body.style.overflow = 'hidden';
// Show popup
popup.classList.remove('pointer-events-none');
popup.classList.add('pointer-events-auto');
// Trigger animations
setTimeout(() => {
popup.classList.remove('opacity-0');
popup.classList.add('opacity-100');
if (content) {
content.classList.remove('scale-95');
content.classList.add('scale-100');
}
}, 10);
}
function closeNavigationPopup() {
const popup = document.getElementById('navigationPopup');
popup.classList.add('hidden');
const content = popup.querySelector('.relative');
// Restore body scrolling
document.body.style.overflow = '';
// Start close animation
popup.classList.remove('opacity-100');
popup.classList.add('opacity-0');
if (content) {
content.classList.remove('scale-100');
content.classList.add('scale-95');
}
// Hide after animation
setTimeout(() => {
popup.classList.remove('pointer-events-auto');
popup.classList.add('pointer-events-none');
}, 300);
}
function toggleNavigationPopup() {
const popup = document.getElementById('navigationPopup');
const isOpen = popup.classList.contains('pointer-events-auto');
if (isOpen) {
closeNavigationPopup();
} else {
openNavigationPopup();
}
}
// Menu button click handler
document.addEventListener('DOMContentLoaded', function() {
const menuButton = document.getElementById('menuButton');
menuButton.addEventListener('click', openNavigationPopup);
menuButton.addEventListener('click', toggleNavigationPopup);
});
// Popup notification system with Tailwind classes

View File

@@ -1,6 +1,6 @@
{{template "base.html" .}}
{{define "title"}}DNS Tools - HeaderAnalyzer{{end}}
{{define "title"}}DNS Tools{{end}}
{{define "content"}}
<div class="container mx-auto px-4 py-8 max-w-6xl">

View File

@@ -1,224 +0,0 @@
{{template "base.html" .}}
{{define "title"}}DNS Tools - HeaderAnalyzer{{end}}
{{define "content"}}
<div class="dns-tools-container">
<div class="bg-dark-bg rounded-xl p-8 border border-dark-border">
<h1 class="text-3xl font-bold text-dark-text mb-2">🌐 DNS Tools</h1>
<p class="text-dark-muted mb-6">Comprehensive DNS lookup and analysis tools for domains and IP addresses</p>
<form class="dns-query-form" id="dnsForm" onsubmit="return false;">
<input type="text" id="dnsInput" placeholder="Enter domain or IP" required
class="flex-1 min-w-48 px-4 py-3 bg-dark-bg border border-dark-border rounded-lg text-dark-text placeholder-dark-muted focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20">
<select id="dnsType" class="px-4 py-3 bg-dark-bg border border-dark-border rounded-lg text-dark-text focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20">
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
<option value="NS">NS</option>
<option value="CNAME">CNAME</option>
<option value="PTR">PTR</option>
<option value="SOA">SOA</option>
<option value="SPF">SPF</option>
<option value="DKIM">DKIM</option>
<option value="DMARC">DMARC</option>
<option value="WHOIS">WHOIS</option>
</select>
<input type="text" id="dnsServer" placeholder="Custom DNS server (optional)"
class="w-48 px-4 py-3 bg-dark-bg border border-dark-border rounded-lg text-dark-text placeholder-dark-muted focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20">
<button type="submit" class="btn btn-primary">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Query
</button>
</form>
<div class="save-btns">
<button onclick="saveResults('csv')" class="btn btn-success">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Save as CSV
</button>
<button onclick="saveResults('txt')" class="btn btn-success">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Save as TXT
</button>
</div>
</div>
<div class="dns-results" id="dnsResults"></div>
</div>
<!-- Popup notification container -->
<div id="popup-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
{{end}}
{{define "scripts"}}
<script>
let results = [];
// 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>
<div class="flex items-start gap-2">
<div class="flex-shrink-0 mt-0.5">
${type === 'error' ? '❌' : type === 'success' ? '✅' : type === 'warning' ? '⚠️' : ''}
</div>
<div class="flex-1">${message}</div>
</div>
`;
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);
}
}
document.getElementById('dnsForm').addEventListener('submit', function() {
const query = document.getElementById('dnsInput').value.trim();
const type = document.getElementById('dnsType').value;
const server = document.getElementById('dnsServer').value.trim();
if (!query) {
showPopup('Please enter a domain or IP address to query', 'warning');
return;
}
let url = `/api/dns?query=${encodeURIComponent(query)}&type=${encodeURIComponent(type)}`;
if (server) {
url += `&server=${encodeURIComponent(server)}`;
}
// Add selector field for DKIM queries
if (type === 'DKIM' && !query.includes(':')) {
const selector = prompt('Enter DKIM selector (e.g., "selector1", "default"):');
if (selector) {
url = `/api/dns?query=${encodeURIComponent(query + ':' + selector)}&type=${encodeURIComponent(type)}`;
if (server) {
url += `&server=${encodeURIComponent(server)}`;
}
} else {
showPopup('DKIM selector is required for DKIM queries', 'warning');
return;
}
}
// Show loading state
const submitBtn = document.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = `
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Querying...
`;
submitBtn.disabled = true;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
return response.text();
})
.then(data => {
const timestamp = new Date().toLocaleString();
const result = {
timestamp: timestamp,
query: query,
type: type,
server: server || 'Default',
result: data
};
results.push(result);
const resultDiv = document.createElement('div');
resultDiv.className = 'dns-result-block';
resultDiv.innerHTML = `
<h3 class="text-blue-400 font-semibold mb-2">${type} query for ${query}</h3>
<p class="text-dark-muted text-sm mb-3">Time: ${timestamp} | Server: ${server || 'Default'}</p>
<pre class="bg-gray-900 text-dark-text p-3 rounded-lg overflow-x-auto text-sm">${data}</pre>
`;
document.getElementById('dnsResults').insertBefore(resultDiv, document.getElementById('dnsResults').firstChild);
showPopup(`Successfully queried ${type} record for ${query}`, 'success', 3000);
})
.catch(error => {
console.error('DNS Query Error:', error);
const resultDiv = document.createElement('div');
resultDiv.className = 'dns-result-block border-red-500/50';
resultDiv.innerHTML = `
<h3 class="text-red-400 font-semibold mb-2">❌ Error querying ${query}</h3>
<pre class="bg-gray-900 text-red-300 p-3 rounded-lg text-sm">Error: ${error.message}</pre>
`;
document.getElementById('dnsResults').insertBefore(resultDiv, document.getElementById('dnsResults').firstChild);
showPopup(`Failed to query ${query}: ${error.message}`, 'error');
})
.finally(() => {
// Restore button
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
function saveResults(format) {
if (results.length === 0) {
showPopup('No results to save. Please run some DNS queries first.', 'warning');
return;
}
let content = '';
let filename = '';
try {
if (format === 'csv') {
content = 'Timestamp,Query,Type,Server,Result\n';
results.forEach(r => {
const escapedResult = '"' + r.result.replace(/"/g, '""') + '"';
content += `"${r.timestamp}","${r.query}","${r.type}","${r.server}",${escapedResult}\n`;
});
filename = 'dns-results.csv';
} else if (format === 'txt') {
results.forEach(r => {
content += `=== ${r.type} query for ${r.query} ===\n`;
content += `Time: ${r.timestamp}\n`;
content += `Server: ${r.server}\n`;
content += `Result:\n${r.result}\n\n`;
});
filename = 'dns-results.txt';
}
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
showPopup(`Successfully exported ${results.length} results as ${format.toUpperCase()}`, 'success', 3000);
} catch (error) {
showPopup(`Failed to export results: ${error.message}`, 'error');
}
}
</script>
{{end}}

View File

@@ -40,11 +40,17 @@
{{end}}
{{if .From}}
<!-- Hidden form for refresh functionality -->
<form id="refreshForm" method="POST" style="display: none;">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<textarea name="headers" style="display: none;">{{.OriginalHeaders}}</textarea>
</form>
<div id="report" class="space-y-6">
<!-- Sender Identification -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-4">👤 Sender Identification</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="grid grid-cols-2 gap-6" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-400">Envelope Sender (Return-Path):</span>
@@ -161,7 +167,7 @@
<!-- Basic Information -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-4">📧 Basic Information</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="grid grid-cols-2 gap-6">
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-400">From:</span>
@@ -246,7 +252,7 @@
<!-- Security Analysis -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔒 Security Analysis</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="grid grid-cols-3 gap-6">
<!-- SPF Authentication -->
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
<h3 class="text-lg font-semibold text-gray-200 mb-3">SPF Authentication</h3>
@@ -595,10 +601,10 @@
}
function exportImage() {
if (typeof html2canvas === 'undefined') {
alert('Image export library not loaded. Please refresh the page and try again.');
return;
}
// if (typeof html2canvas === 'undefined') {
// alert('Image export library not loaded. Please refresh the page and try again.');
// return;
// }
html2canvas(document.querySelector("#report")).then(canvas => {
let link = document.createElement("a");
@@ -639,19 +645,19 @@
function exportPDF() {
// Check if libraries are available
if (typeof html2canvas === 'undefined') {
alert('HTML2Canvas library not loaded. Please refresh the page and try again.');
return;
}
// if (typeof html2canvas === 'undefined') {
// alert('HTML2Canvas library not loaded. Please refresh the page and try again.');
// return;
// }
// Check if we have either html2pdf or jsPDF available
const hasHtml2pdf = typeof html2pdf !== 'undefined';
const hasJsPDF = typeof window.jsPDF !== 'undefined';
if (!hasHtml2pdf && !hasJsPDF) {
alert('PDF generation library not loaded. Please refresh the page and try again.');
return;
}
// if (!hasHtml2pdf && !hasJsPDF) {
// alert('PDF generation library not loaded. Please refresh the page and try again.');
// return;
// }
// Store original states
const originalDetailsStates = {};
@@ -903,7 +909,6 @@
}
} catch (error) {
console.error('PDF export error:', error);
alert('PDF export failed. Please refresh the page and try again.');
restoreOriginalState();
}

View File

@@ -1,6 +1,6 @@
{{template "base.html" .}}
{{define "title"}}HeaderAnalyzer - IT & Security Tools{{end}}
{{define "title"}}GoNetKit{{end}}
{{define "head"}}
<style>
@@ -24,19 +24,19 @@
{{end}}
{{define "content"}}
<div class="min-h-screen">
<div>
<!-- Hero Section -->
<div class="text-center mb-16">
<div class="gradient-bg rounded-3xl p-12 mb-8 shadow-2xl">
<div class="gradient-bg rounded-3xl p-6 mb-4 shadow-2xl">
<h1 class="text-5xl md:text-6xl font-bold text-white" style="border-bottom: none;">IT & Security Tools</h1>
<!--
<p class="text-xl md:text-2xl text-blue-100 mb-8 max-w-3xl mx-auto">
Your comprehensive toolkit for IT diagnostics and online security
</p>
<div class="flex flex-wrap justify-center gap-4 text-sm text-blue-200">
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Email Security</span>
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Password Security</span>
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Data Protection</span>
<span class="bg-white/20 px-4 py-2 rounded-full">Email Security</span>
<span class="bg-white/20 px-4 py-2 rounded-full">Password Security</span>
<span class="bg-white/20 px-4 py-2 rounded-full">Data Protection</span>
</div>
-->
</div>
@@ -103,7 +103,7 @@
<!-- Online Security Category -->
<div class="category-section">
<div class="flex items-center mb-8">
<div class="bg-gradient-to-r from-orange-500 to-red-500 text-white px-4 py-2 rounded-full text-sm font-semibold mr-4">
<div class="category-badge text-white px-4 py-2 rounded-full text-sm font-semibold mr-4">
ONLINE SECURITY
</div>
<div class="flex-1 h-px bg-dark-border"></div>

View File

@@ -1,781 +0,0 @@
{{template "base.html" .}}
{{define "title"}}Password Generator - HeaderAnalyzer{{end}}
{{define "content"}}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<div class="text-center mb-8">
<a href="/pwgenerator" class="inline-block">
<h1 class="text-2xl md:text-3xl font-bold text-gray-100 hover:text-blue-400 transition-colors cursor-pointer mb-4">
🔐 Password Generator
</h1>
</a>
</div>
<!-- Hidden CSRF token for API calls -->
<input type="hidden" id="csrfToken" value="{{.CSRFToken}}">
<!-- Tab Buttons -->
<div class="flex space-x-2 mb-6 bg-gray-800 p-2 rounded-lg border border-gray-700">
<button class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" id="randomTab">
🎲 Random Password
</button>
<button class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-colors bg-blue-600 text-white" id="passphraseTab">
📝 Passphrase
</button>
</div>
<!-- Password Output -->
<!-- Generated Password Display -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<h2 class="text-xl font-semibold text-gray-200">🔐 Generated Password</h2>
<div class="flex items-center gap-4 text-sm">
<span id="characterCount" class="text-gray-400 bg-gray-700 px-3 py-1 rounded-full">
0 characters
</span>
<span id="strengthDisplay" class="text-gray-400 bg-gray-700 px-3 py-1 rounded-full">
Not generated
</span>
</div>
</div>
<div class="relative">
<input type="text" id="generatedPassword" readonly
class="w-full p-4 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 font-mono text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Click 'Generate Password' to create a new password">
<button onclick="copyPassword()"
class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
📋 Copy
</button>
</div>
</div>
<!-- Generate Button -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8">
<button onclick="generatePassword()"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-8 rounded-lg text-lg transition-all duration-200 transform hover:scale-105">
🎲 Generate Password
</button>
<button onclick="copyCurrentURL()"
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-4 px-6 rounded-lg transition-colors duration-200">
🔗 Copy URL with Settings
</button>
<button onclick="resetAllSettings()"
class="bg-red-600 hover:bg-red-700 text-white font-medium py-4 px-6 rounded-lg transition-colors duration-200">
🔄 Reset
</button>
</div>
<!-- Controls -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Basic Settings -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="text-xl font-semibold text-gray-200 mb-4">🔧 Basic Settings</h3>
<div class="space-y-4">
<!-- Save Passwords Option (moved from Settings Management) -->
<div class="p-3 bg-gray-900 rounded-lg border border-gray-600">
<div class="flex items-center space-x-3">
<input type="checkbox" id="savePasswords" {{if .Config.SavePasswords}}checked{{end}} onchange="togglePasswordSaving(); autoSaveSettings()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="savePasswords" class="text-sm text-gray-300">
Save Generated Passwords (in web browser cookies only)
</label>
</div>
</div>
<div>
<label for="length" class="block text-sm font-medium text-gray-300 mb-2">Password Length:</label>
<input type="number" id="length" min="4" max="128" value="{{.Config.Length}}" onchange="updateURL(); autoSaveSettings()"
class="w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
</div>
<div class="flex items-center space-x-3">
<input type="checkbox" id="includeUpper" {{if .Config.IncludeUpper}}checked{{end}} onchange="updateURL(); autoSaveSettings()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="includeUpper" class="text-sm text-gray-300">Include Uppercase (A-Z)</label>
</div>
<div class="flex items-center space-x-3">
<input type="checkbox" id="includeLower" {{if .Config.IncludeLower}}checked{{end}} onchange="updateURL(); autoSaveSettings()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="includeLower" class="text-sm text-gray-300">Include Lowercase (a-z)</label>
</div>
<div>
<label for="numberCount" class="block text-sm font-medium text-gray-300 mb-2">Number of Digits:</label>
<input type="number" id="numberCount" min="0" max="20" value="{{.Config.NumberCount}}" onchange="updateURL(); autoSaveSettings()"
class="w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
</div>
<div>
<label for="specialChars" class="block text-sm font-medium text-gray-300 mb-2">Special Characters:</label>
<input type="text" id="specialChars" value="{{.Config.SpecialChars}}"
onchange="validateSpecialChars(); updateURL(); autoSaveSettings()"
oninput="validateSpecialChars()"
pattern="[!@#$%&*\-_=+.]*"
title="Only these special characters are allowed: !@#$%&*-_=+."
class="w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 font-mono focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
<div id="specialCharsError" class="text-red-400 text-sm mt-1 hidden">
Only these special characters are allowed: !@#$%&*-_=+.
</div>
<div class="text-gray-500 text-xs mt-1">
Allowed: !@#$%&*-_=+.
</div>
</div>
<div class="flex items-center space-x-3">
<input type="checkbox" id="noConsecutive" {{if .Config.NoConsecutive}}checked{{end}} onchange="updateURL(); autoSaveSettings()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="noConsecutive" class="text-sm text-gray-300">No consecutive identical characters</label>
</div>
</div>
</div>
<!-- Advanced Settings -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 class="text-xl font-semibold text-gray-200 mb-4">🎯 Advanced Settings</h3>
<div class="space-y-4">
<!-- Passphrase Controls -->
<div class="{{if eq .Config.Type "passphrase"}}block{{else}}hidden{{end}}" id="passphraseControls">
<div class="space-y-4 p-4 bg-gray-900 rounded-lg border border-gray-600">
<h4 class="text-lg font-medium text-gray-200">📝 Passphrase Options</h4>
<div>
<label for="wordCount" class="block text-sm font-medium text-gray-300 mb-2">Number of Words:</label>
<input type="number" id="wordCount" min="2" max="10" value="{{.Config.WordCount}}" onchange="updateURL()"
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
</div>
<div class="flex items-center space-x-3">
<input type="checkbox" id="passphraseUseNumbers" {{if .Config.UseNumbers}}checked{{end}} onchange="updateURL()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="passphraseUseNumbers" class="text-sm text-gray-300">Include Numbers</label>
</div>
<div class="flex items-center space-x-3">
<input type="checkbox" id="passphraseUseSpecial" {{if .Config.UseSpecial}}checked{{end}} onchange="updateURL()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
<label for="passphraseUseSpecial" class="text-sm text-gray-300">Include Special Characters</label>
</div>
<div>
<label for="numberPosition" class="block text-sm font-medium text-gray-300 mb-2">Number Position:</label>
<select id="numberPosition" onchange="updateURL()"
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
<option value="end" {{if eq .Config.NumberPosition "end"}}selected{{end}}>At End</option>
<option value="start" {{if eq .Config.NumberPosition "start"}}selected{{end}}>At Start</option>
<option value="each" {{if eq .Config.NumberPosition "each"}}selected{{end}}>After Each Word</option>
</select>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Strength Indicator:</label>
<div id="strengthIndicator" class="text-gray-400 p-3 bg-gray-900 rounded-lg border border-gray-600">
Generate a password to see strength
</div>
</div>
</div>
</div>
</div>
<!-- Password History -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mt-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-gray-200">📚 Password History</h3>
<button onclick="clearHistory()"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-500/20">
🗑️ Clear History
</button>
</div>
<div id="passwordHistory" class="bg-gray-900 border border-gray-600 rounded-lg p-4">
<p class="text-gray-400 italic">No passwords generated yet</p>
</div>
</div>
</div>
{{end}}
</div>
</div>
{{define "scripts"}}
<script>
let currentMode = 'passphrase'; // Default to passphrase
// Initialize the interface based on saved settings or URL parameters
document.addEventListener('DOMContentLoaded', function() {
// Load settings first (from URL parameters or cookies)
loadSettings();
// Update URL to reflect current state
updateURL();
// Load password history
loadPasswordHistory();
// Auto-generate if URL has parameters (excluding default)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
generatePassword();
}
// Note: Removed auto-save event listeners to prevent excessive saving notifications
});
// Tab switching
function switchTab(mode) {
currentMode = mode;
// Get tab elements
const randomTab = document.getElementById('randomTab');
const passphraseTab = document.getElementById('passphraseTab');
// Remove active classes from both tabs
randomTab.classList.remove('bg-blue-600', 'text-white');
randomTab.classList.add('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
passphraseTab.classList.remove('bg-blue-600', 'text-white');
passphraseTab.classList.add('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
// Add active classes to the selected tab
if (mode === 'random') {
randomTab.classList.remove('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
randomTab.classList.add('bg-blue-600', 'text-white');
} else {
passphraseTab.classList.remove('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
passphraseTab.classList.add('bg-blue-600', 'text-white');
}
// Show/hide passphrase controls
document.getElementById('passphraseControls').classList.toggle('block', mode === 'passphrase');
document.getElementById('passphraseControls').classList.toggle('hidden', mode !== 'passphrase');
updateURL();
autoSaveSettings(); // Auto-save without notification
}
document.getElementById('randomTab').addEventListener('click', () => switchTab('random'));
document.getElementById('passphraseTab').addEventListener('click', () => switchTab('passphrase'));
// Validate special characters input
function validateSpecialChars() {
const input = document.getElementById('specialChars');
const errorDiv = document.getElementById('specialCharsError');
const allowedChars = '!@#$%&*-_=+.';
const value = input.value;
let isValid = true;
for (let i = 0; i < value.length; i++) {
if (!allowedChars.includes(value[i])) {
isValid = false;
break;
}
}
if (isValid) {
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
input.classList.add('border-gray-600', 'focus:border-blue-500', 'focus:ring-blue-500/20');
errorDiv.classList.add('hidden');
} else {
input.classList.remove('border-gray-600', 'focus:border-blue-500', 'focus:ring-blue-500/20');
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
errorDiv.classList.remove('hidden');
}
return isValid;
}
// URL parameter management
function updateURL() {
const config = getCurrentConfig();
const params = new URLSearchParams();
// Define default values (savePasswords excluded from URL parameters)
const defaults = {
type: "passphrase",
length: 12,
includeUpper: true,
includeLower: true,
numberCount: 1,
specialChars: "!@#$%&*-_=+.",
noConsecutive: false,
wordCount: 3,
useNumbers: true,
useSpecial: false,
numberPosition: "end"
};
// Only add parameters that differ from defaults (excluding savePasswords)
Object.keys(config).forEach(key => {
if (key !== 'savePasswords' && config[key] !== defaults[key]) {
params.set(key, config[key]);
}
});
// Update the URL without causing a page reload
const queryString = params.toString();
const newURL = queryString ? window.location.pathname + '?' + queryString : window.location.pathname;
window.history.replaceState({}, '', newURL);
}
function getCurrentConfig() {
return {
type: currentMode,
length: parseInt(document.getElementById('length').value),
includeUpper: document.getElementById('includeUpper').checked,
includeLower: document.getElementById('includeLower').checked,
numberCount: parseInt(document.getElementById('numberCount').value),
specialChars: document.getElementById('specialChars').value,
noConsecutive: document.getElementById('noConsecutive').checked,
wordCount: parseInt(document.getElementById('wordCount').value),
useNumbers: document.getElementById('passphraseUseNumbers').checked,
useSpecial: document.getElementById('passphraseUseSpecial').checked,
numberPosition: document.getElementById('numberPosition').value,
savePasswords: document.getElementById('savePasswords').checked
};
}
// Cookie management
function saveSettings() {
const config = getCurrentConfig();
config.mode = currentMode;
const settings = JSON.stringify(config);
// Set cookie to expire in 1 year
const expiryDate = new Date();
expiryDate.setFullYear(expiryDate.getFullYear() + 1);
document.cookie = `passwordGenSettings=${encodeURIComponent(settings)}; expires=${expiryDate.toUTCString()}; path=/`;
showNotification('Settings saved! They will be remembered when you visit this page again.', 'success');
}
// Auto-save function without showing notification
function autoSaveSettings() {
const config = getCurrentConfig();
config.mode = currentMode;
const settings = JSON.stringify(config);
// Set cookie to expire in 1 year
const expiryDate = new Date();
expiryDate.setFullYear(expiryDate.getFullYear() + 1);
document.cookie = `passwordGenSettings=${encodeURIComponent(settings)}; expires=${expiryDate.toUTCString()}; path=/`;
}
// Copy current URL with settings
function copyCurrentURL() {
updateURL(); // Ensure URL is up to date
const currentURL = window.location.href;
navigator.clipboard.writeText(currentURL).then(function() {
showPopup('URL with current settings copied to clipboard!', 'success');
}, function(err) {
console.error('Could not copy URL: ', err);
showPopup('Failed to copy URL to clipboard', 'error');
});
}
// Reset all settings and cookies
function resetAllSettings() {
showConfirmationPopup(
'Reset All Settings?',
'This will clear all saved settings and password history, returning the page to its default state.',
function() {
// Clear all cookies
document.cookie = 'passwordGenSettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie = 'passwordHistory=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
showPopup('All settings and history cleared. Redirecting to clean page...', 'info');
setTimeout(() => {
// Redirect to the page without any parameters
window.location.href = window.location.pathname;
}, 1500);
}
);
}
function loadSettings() {
// First try URL parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
const config = {};
for (const [key, value] of urlParams) {
// Skip savePasswords from URL parameters - it's cookie-only
if (key === 'savePasswords') continue;
if (key === 'type') config[key] = value;
else if (key === 'length' || key === 'numberCount' || key === 'wordCount') config[key] = parseInt(value);
else if (key === 'includeUpper' || key === 'includeLower' || key === 'noConsecutive' ||
key === 'useNumbers' || key === 'useSpecial') config[key] = value === 'true';
else config[key] = value;
}
applyConfig(config);
// Load savePasswords setting separately from cookies only
loadSavePasswordsSetting();
return;
}
// Then try cookies
const settings = getCookie('passwordGenSettings');
if (settings) {
try {
const config = JSON.parse(decodeURIComponent(settings));
applyConfig(config);
} catch (e) {
console.error('Failed to parse saved settings:', e);
}
}
}
// Load savePasswords setting from cookies only (never from URL)
function loadSavePasswordsSetting() {
const settings = getCookie('passwordGenSettings');
if (settings) {
try {
const config = JSON.parse(decodeURIComponent(settings));
if (config.savePasswords !== undefined) {
document.getElementById('savePasswords').checked = config.savePasswords;
// Update history display based on the setting
if (config.savePasswords) {
const history = getPasswordHistory();
displayPasswordHistory(history);
} else {
document.getElementById('passwordHistory').innerHTML = '<p style="color: #999; font-style: italic;">Password saving is disabled</p>';
}
}
} catch (e) {
console.error('Failed to parse saved settings for savePasswords:', e);
}
}
}
// Custom confirmation popup function
function showConfirmationPopup(title, message, onConfirm) {
// Create backdrop
const backdrop = document.createElement('div');
backdrop.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center p-4';
// Create popup
backdrop.innerHTML = `
<div class="bg-gray-800 border border-gray-600 rounded-xl p-6 max-w-md w-full shadow-2xl transform transition-all">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-red-400">${title}</h3>
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-200 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<p class="text-gray-300 mb-6">${message}</p>
<div class="flex space-x-3 justify-end">
<button onclick="this.closest('.fixed').remove()"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
Cancel
</button>
<button onclick="confirmAction()"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Yes, Clear All
</button>
</div>
</div>
`;
// Add confirm action to the backdrop element
backdrop.confirmCallback = onConfirm;
// Add global confirmAction function temporarily
window.confirmAction = function() {
backdrop.confirmCallback();
backdrop.remove();
delete window.confirmAction;
};
// Add to page
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.addEventListener('click', function(e) {
if (e.target === backdrop) {
backdrop.remove();
delete window.confirmAction;
}
});
}
function applyConfig(config) {
// Apply the configuration to form controls
currentMode = config.type || config.mode || 'passphrase';
document.getElementById('length').value = config.length || 12;
document.getElementById('includeUpper').checked = config.includeUpper !== false;
document.getElementById('includeLower').checked = config.includeLower !== false;
document.getElementById('numberCount').value = config.numberCount || 1;
document.getElementById('specialChars').value = config.specialChars || "!@#$%&*-_=+.";
document.getElementById('noConsecutive').checked = config.noConsecutive || false;
document.getElementById('wordCount').value = config.wordCount || 3;
document.getElementById('passphraseUseNumbers').checked = config.useNumbers !== false;
document.getElementById('passphraseUseSpecial').checked = config.useSpecial || false;
document.getElementById('numberPosition').value = config.numberPosition || "end";
document.getElementById('savePasswords').checked = config.savePasswords || false;
// Update tab state using the switchTab function to ensure proper styling
switchTab(currentMode);
}
// Password history management
function addToHistory(password) {
// Check if password saving is enabled
const savePasswords = document.getElementById('savePasswords').checked;
if (!savePasswords) {
return; // Don't save if checkbox is unchecked
}
let history = getPasswordHistory();
const timestamp = new Date().toLocaleString();
const entry = { password, timestamp, type: currentMode };
// Add to beginning of array
history.unshift(entry);
// Keep only last 20 passwords
if (history.length > 20) {
history = history.slice(0, 20);
}
// Save to cookie
const historyData = JSON.stringify(history);
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + 3); // 3 months
document.cookie = `passwordHistory=${encodeURIComponent(historyData)}; expires=${expiryDate.toUTCString()}; path=/`;
// Update display
displayPasswordHistory(history);
}
function getPasswordHistory() {
const historyData = getCookie('passwordHistory');
if (historyData) {
try {
return JSON.parse(decodeURIComponent(historyData));
} catch (e) {
return [];
}
}
return [];
}
function loadPasswordHistory() {
const savePasswords = document.getElementById('savePasswords').checked;
if (savePasswords) {
const history = getPasswordHistory();
displayPasswordHistory(history);
} else {
const historyDiv = document.getElementById('passwordHistory');
historyDiv.innerHTML = '<p style="color: #999; font-style: italic;">Password saving is disabled</p>';
}
}
function togglePasswordSaving() {
const savePasswords = document.getElementById('savePasswords').checked;
const historyDiv = document.getElementById('passwordHistory');
const viewHistoryBtn = document.getElementById('viewHistoryBtn');
if (savePasswords) {
// Re-display existing history
const history = getPasswordHistory();
displayPasswordHistory(history);
showNotification('Password saving enabled', 'success');
// Show View History button if there's a password displayed
const passwordDisplay = document.getElementById('passwordDisplay').textContent;
if (passwordDisplay && passwordDisplay !== 'Click "Generate Password" to create a secure password') {
viewHistoryBtn.style.display = 'inline-block';
}
} else {
// Clear stored passwords and hide history display
document.cookie = 'passwordHistory=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
historyDiv.innerHTML = '<p style="color: #999; font-style: italic;">Password saving is disabled</p>';
showNotification('Password saving disabled - history cleared', 'info');
// Hide View History button
viewHistoryBtn.style.display = 'none';
}
// Auto-save the setting change without notification
autoSaveSettings();
}
function displayPasswordHistory(history) {
const historyDiv = document.getElementById('passwordHistory');
if (history.length === 0) {
historyDiv.innerHTML = '<p style="color: #999; font-style: italic;">No passwords generated yet</p>';
return;
}
let html = '';
history.forEach((entry, index) => {
const shortPassword = entry.password.length > 30 ? entry.password.substring(0, 30) + '...' : entry.password;
html += `
<div style="margin-bottom: 10px; padding: 8px; background: #2a2a2a; border-radius: 4px; display: flex; align-items: center; justify-content: space-between;">
<div style="flex: 1;">
<div style="font-family: monospace; color: #00ff88; margin-bottom: 2px;">${shortPassword}</div>
<div style="font-size: 12px; color: #999;">${entry.type}${entry.timestamp}</div>
</div>
<button onclick="copyHistoryPassword('${entry.password.replace(/'/g, "\\'")}', ${index})"
style="background: #007acc; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; margin-left: 10px;">
Copy
</button>
</div>
`;
});
historyDiv.innerHTML = html;
}
function copyHistoryPassword(password, index) {
navigator.clipboard.writeText(password).then(function() {
// Temporarily change button text
const buttons = document.querySelectorAll('#passwordHistory button');
if (buttons[index]) {
const originalText = buttons[index].textContent;
buttons[index].textContent = 'Copied!';
buttons[index].style.background = '#00aa44';
setTimeout(function() {
buttons[index].textContent = originalText;
buttons[index].style.background = '#007acc';
}, 1500);
}
});
}
function clearHistory() {
showConfirmationPopup(
'Clear Password History?',
'This will permanently delete all saved passwords from your browser. This action cannot be undone.',
function() {
// User confirmed
document.cookie = 'passwordHistory=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
loadPasswordHistory();
showPopup('Password history cleared.', 'info');
}
);
}
// Utility function to get cookie value
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
function generatePassword() {
// Validate special characters before generating
if (!validateSpecialChars()) {
showNotification('Please fix special characters field before generating password', 'error');
return;
}
const config = getCurrentConfig();
fetch('/api/pwgenerator', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config)
})
.then(response => response.text())
.then(password => {
document.getElementById('generatedPassword').value = password;
// Update character count
document.getElementById('characterCount').textContent = `${password.length} characters`;
// Calculate and display strength
const strengthInfo = calculatePasswordStrength(password);
document.getElementById('strengthIndicator').innerHTML = strengthInfo.html;
document.getElementById('strengthDisplay').textContent = strengthInfo.text;
// Add to history
addToHistory(password);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('generatedPassword').value = 'Error generating password';
document.getElementById('characterCount').textContent = '0 characters';
document.getElementById('strengthDisplay').textContent = 'Error';
});
}
function copyPassword() {
const password = document.getElementById('generatedPassword').value;
if (password && password !== 'Click \'Generate Password\' to create a new password') {
navigator.clipboard.writeText(password).then(function() {
showNotification('Password copied to clipboard!', 'success');
}, function(err) {
console.error('Could not copy text: ', err);
showNotification('Failed to copy password', 'error');
});
}
}
function scrollToHistory() {
const historyElement = document.getElementById('passwordHistory');
if (historyElement) {
historyElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
function calculatePasswordStrength(password) {
let score = 0;
let feedback = [];
// Length scoring
if (password.length >= 12) score += 25;
else if (password.length >= 8) score += 15;
else if (password.length >= 6) score += 10;
else feedback.push("Too short (< 6 chars)");
// Character variety
if (/[a-z]/.test(password)) score += 15;
if (/[A-Z]/.test(password)) score += 15;
if (/[0-9]/.test(password)) score += 15;
if (/[^A-Za-z0-9]/.test(password)) score += 15;
// Complexity patterns
if (password.length > 8 && /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])/.test(password)) score += 15;
let strength = "Very Weak";
let color = "#ff4444";
if (score >= 80) { strength = "Very Strong"; color = "#00ff88"; }
else if (score >= 60) { strength = "Strong"; color = "#88ff00"; }
else if (score >= 40) { strength = "Moderate"; color = "#ffaa00"; }
else if (score >= 20) { strength = "Weak"; color = "#ff8800"; }
return {
html: `<span style="color: ${color}; font-weight: bold;">${strength}</span> (${score}/100)`,
text: strength
};
}
// Use the showPopup function from base.html instead of custom notifications
function showNotification(message, type = 'info') {
showPopup(message, type);
}
</script>
{{end}}

View File

@@ -1,12 +1,12 @@
{{template "base.html" .}}
{{define "title"}}Password Generator - HeaderAnalyzer{{end}}
{{define "title"}}Password Generator{{end}}
{{define "content"}}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<div class="text-center mb-8">
<div class="container mx-auto px-4 py-6 max-w-4xl">
<div class="text-center mb-6">
<a href="/pwgenerator" class="inline-block">
<h1 class="text-2xl md:text-3xl font-bold text-gray-100 hover:text-blue-400 transition-colors cursor-pointer mb-4">
<h1 class="text-2xl md:text-3xl font-bold text-gray-100 hover:text-blue-400 transition-colors cursor-pointer mb-3">
🔐 Password Generator
</h1>
</a>
@@ -15,61 +15,59 @@
<input type="hidden" id="csrfToken" value="{{.CSRFToken}}">
<!-- Tab Buttons -->
<div class="flex space-x-2 mb-6 bg-gray-800 p-2 rounded-lg border border-gray-700">
<button class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" id="randomTab">
<div class="flex space-x-2 mb-4 bg-gray-800 p-2 rounded-lg border border-gray-700">
<button class="flex-1 py-2 px-3 rounded-lg text-center font-medium transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600" id="randomTab">
🎲 Random Password
</button>
<button class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-colors bg-blue-600 text-white" id="passphraseTab">
<button class="flex-1 py-2 px-3 rounded-lg text-center font-medium transition-colors bg-blue-600 text-white" id="passphraseTab">
📝 Passphrase
</button>
</div>
<!-- Password Output -->
<!-- Generated Password Display -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<h2 class="text-xl font-semibold text-gray-200">🔐 Generated Password</h2>
<div class="flex items-center gap-4 text-sm">
<span id="characterCount" class="text-gray-400 bg-gray-700 px-3 py-1 rounded-full">
0 characters
</span>
</div>
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-6">
<div class="flex items-center justify-between gap-3 mb-3">
<h2 class="text-lg font-semibold text-gray-200">🔐 Generated Password</h2>
<span id="characterCount" class="text-gray-400 bg-gray-700 px-3 py-1 rounded-full text-sm flex-shrink-0">
0 characters
</span>
</div>
<div class="relative">
<input type="text" id="generatedPassword" readonly
class="w-full p-4 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 font-mono text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full p-3 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 font-mono text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Click 'Generate Password' to create a new password">
<button onclick="copyPassword()"
class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
class="absolute right-2 top-1/2 -translate-y-1/2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
📋 Copy
</button>
</div>
</div>
<!-- Generate Button -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8">
<div class="flex flex-row items-stretch justify-center gap-3 mb-6">
<button onclick="generatePassword()"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-8 rounded-lg text-lg transition-all duration-200 transform hover:scale-105">
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-base transition-all duration-200 hover:scale-105 flex-shrink-0">
🎲 Generate Password
</button>
<button onclick="copyCurrentURL()"
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-4 px-6 rounded-lg transition-colors duration-200">
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg text-base transition-colors duration-200 flex-shrink-0">
🔗 Copy URL with Settings
</button>
<button onclick="resetAllSettings()"
class="bg-red-600 hover:bg-red-700 text-white font-medium py-4 px-6 rounded-lg transition-colors duration-200">
class="bg-red-600 hover:bg-red-700 text-white font-medium py-3 px-6 rounded-lg text-base transition-colors duration-200 flex-shrink-0">
🔄 Reset
</button>
</div>
<!-- Settings -->
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
<div class="flex items-center justify-between mb-4 cursor-pointer" onclick="toggleSettings()">
<h3 class="text-xl font-semibold text-gray-200">🔧 Password Settings</h3>
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-6">
<div class="flex items-center justify-between mb-3 cursor-pointer" onclick="toggleSettings()">
<h3 class="text-lg font-semibold text-gray-200">🔧 Password Settings</h3>
<div class="flex items-center space-x-2">
<span id="settingsToggleText" class="text-sm text-gray-400">Click to expand</span>
<svg id="settingsChevron" class="w-5 h-5 text-gray-400 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg id="settingsChevron" class="w-4 h-4 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
@@ -77,7 +75,7 @@
<div id="settingsContent" class="hidden">
<!-- Save Passwords Option -->
<div class="p-3 bg-gray-900 rounded-lg border border-gray-600 mb-6">
<div class="p-2 bg-gray-900 rounded-lg border border-gray-600 mb-4">
<div class="flex items-center space-x-3">
<input type="checkbox" id="savePasswords" onchange="togglePasswordSaving(); autoSaveSettings()"
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
@@ -88,15 +86,15 @@
</div>
<!-- Settings Table -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="grid grid-cols-2 gap-6">
<!-- Left Column: Basic Password Settings -->
<div class="space-y-4">
<h4 class="text-lg font-medium text-gray-200 mb-4 border-b border-gray-600 pb-2">🎲 Random Password Settings</h4>
<div class="space-y-3">
<h4 class="text-base font-medium text-gray-200 mb-2 border-b border-gray-600 pb-1">🎲 Random Password Settings</h4>
<div>
<label for="length" class="block text-sm font-medium text-gray-300 mb-2">Password Length:</label>
<label for="length" class="block text-sm font-medium text-gray-300 mb-1">Password Length:</label>
<input type="number" id="length" min="4" max="128" value="{{.Config.Length}}" onchange="updateURL(); autoSaveSettings()"
class="w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
class="w-full px-2 py-1 bg-gray-900 border border-gray-600 rounded text-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
</div>
<div class="flex items-center space-x-3">
@@ -273,12 +271,12 @@ function toggleSettings() {
if (content.classList.contains('hidden')) {
// Expand
content.classList.remove('hidden');
chevron.style.transform = 'rotate(180deg)';
chevron.classList.add('rotate-180');
toggleText.textContent = 'Click to minimize';
} else {
// Collapse
content.classList.add('hidden');
chevron.style.transform = 'rotate(0deg)';
chevron.classList.remove('rotate-180');
toggleText.textContent = 'Click to expand';
}
}
@@ -488,11 +486,11 @@ function loadSavePasswordsSetting() {
function showConfirmationPopup(title, message, onConfirm) {
// Create backdrop
const backdrop = document.createElement('div');
backdrop.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center p-4';
backdrop.className = 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4';
// Create popup
backdrop.innerHTML = `
<div class="bg-gray-800 border border-gray-600 rounded-xl p-6 max-w-md w-full shadow-2xl transform transition-all">
<div class="bg-gray-800 border border-gray-600 rounded-xl p-6 max-w-md w-full shadow-2xl transition-all">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-red-400">${title}</h3>
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-200 transition-colors">

View File

@@ -4,7 +4,7 @@
{{define "head"}}
{{if .Success}}
<meta name="success-data" content='{"id":"{{.ID}}","url":"{{.PushURL}}","expiresAt":"{{.ExpiresAt}}"}'>
<meta name="success-data" content='{"id":"{{.ID}}","url":"{{.PushURL}}","expiresAt":"{{.ExpiresAt}}","maxViews":"{{.MaxViews}}"}'>
{{end}}
{{end}}
@@ -32,12 +32,18 @@
readonly
onclick="this.select()"
class="flex-1 p-3 bg-gray-900 border-2 border-gray-600 rounded-lg text-gray-100 font-mono text-sm focus:border-green-500 focus:outline-none">
<button onclick="copyToClipboard()"
class="copy-btn bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
📋 Copy
</button>
<div class="flex flex-row items-stretch justify-center gap-3">
<button onclick="copyToClipboard()"
class="copy-btn bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
📋 Copy URL
</button>
<button onclick="copyAsMessage()"
class="copy-message-btn bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
💬 Copy as Message
</button>
</div>
</div>
<p class="text-green-300 text-sm mb-4">🕒 Expires: {{.ExpiresAt}}</p>
<p class="text-green-300 text-sm mb-4">🕒 Expires: {{.ExpiresAt}} or after {{.MaxViews}} views.</p>
<a href="/pwpush"
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors">
Create Another Link
@@ -95,7 +101,8 @@
id="expiry_days"
min="1" max="90" value="7"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
oninput="updateExpiryDisplay(this.value)">
oninput="updateExpiryDisplay(this.value)"
onchange="saveSettingWithDelay('expiry_days', this.value)">
<div class="flex justify-between items-center text-sm">
<span id="expiryDisplay" class="font-bold text-blue-400">7 days</span>
<small class="text-gray-500">Max: 3 months</small>
@@ -109,7 +116,8 @@
id="max_views"
min="1" max="100" value="10"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
oninput="updateViewsDisplay(this.value)">
oninput="updateViewsDisplay(this.value)"
onchange="saveSettingWithDelay('max_views', this.value)">
<div class="flex justify-between items-center text-sm">
<span id="viewsDisplay" class="font-bold text-blue-400">10 views</span>
<small class="text-gray-500">Max: 100 views</small>
@@ -124,7 +132,8 @@
<input type="checkbox"
name="require_click"
checked
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
onchange="saveSettingImmediately('require_click', this.checked)">
<span class="text-sm font-medium text-gray-200">🛡️ Require click to reveal</span>
</label>
<small class="block text-xs text-gray-400 ml-8">Hides content from web crawlers and requires user interaction</small>
@@ -134,7 +143,8 @@
<label class="flex items-center space-x-3 cursor-pointer">
<input type="checkbox"
name="auto_delete"
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
onchange="saveSettingImmediately('auto_delete', this.checked)">
<span class="text-sm font-medium text-gray-200">🗑️ Allow manual deletion</span>
</label>
<small class="block text-xs text-gray-400 ml-8">Adds a delete button when content is viewed (viewer can choose to delete)</small>
@@ -145,7 +155,8 @@
<input type="checkbox"
name="track_history"
id="track_history"
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
onchange="saveSettingImmediately('track_history', this.checked)">
<span class="text-sm font-medium text-gray-200">📚 Save to my history</span>
</label>
<small class="block text-xs text-gray-400 ml-8">Keep a record of your created links (stored in browser)</small>
@@ -155,7 +166,7 @@
</div>
<!-- Action buttons -->
<div class="text-center pt-6 border-t border-gray-700">
<div class="text-center pt-6">
<button type="submit"
class="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-bold py-4 px-8 rounded-lg text-lg transition-all duration-200 transform hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-blue-500/50">
🔒 Create Secure Link
@@ -290,6 +301,30 @@ function updateViewsDisplay(views) {
display.textContent = views + (views === '1' ? ' view' : ' views');
}
// Auto-save settings functionality
let saveTimeouts = {};
function saveSettingWithDelay(settingName, value) {
// Clear any existing timeout for this setting
if (saveTimeouts[settingName]) {
clearTimeout(saveTimeouts[settingName]);
}
// Set a new timeout to save after user stops changing the value
saveTimeouts[settingName] = setTimeout(() => {
saveSettingImmediately(settingName, value);
}, 1000); // Wait 1 second after user stops moving slider
}
function saveSettingImmediately(settingName, value) {
setCookie('pwpush_' + settingName, value, 30);
// Show success notification
if (typeof showPopup === 'function') {
showPopup('Setting "' + settingName.replace('_', ' ') + '" saved automatically!', 'success', 2000);
}
}
function copyToClipboard() {
const urlInput = document.getElementById('pushUrl');
const btn = document.querySelector('.copy-btn');
@@ -350,6 +385,91 @@ function copyToClipboard() {
}
}
function copyAsMessage() {
const urlInput = document.getElementById('pushUrl');
const btn = document.querySelector('.copy-message-btn');
if (!urlInput) {
console.error('pushUrl input not found');
return;
}
if (!btn) {
console.error('copy-message-btn button not found');
return;
}
// Get the data from meta tag
const successData = document.querySelector('meta[name="success-data"]');
if (!successData) {
console.error('No success data found');
return;
}
const data = JSON.parse(successData.content);
const url = data.url;
const expiresAt = data.expiresAt;
const maxViews = data.maxViews;
// Create the message
const message = `The secret has been shared via the link below, it will expire on ${expiresAt} or after ${maxViews} views, whichever comes first:
${url}`;
const originalText = btn.textContent;
// Use modern clipboard API if available, fallback to execCommand
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(message).then(() => {
btn.textContent = '✅ Message Copied!';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
}).catch(err => {
console.error('Failed to copy message: ', err);
fallbackCopyMessage();
});
} else {
fallbackCopyMessage();
}
function fallbackCopyMessage() {
// Create a temporary textarea to copy the message
const textArea = document.createElement('textarea');
textArea.value = message;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
btn.textContent = '✅ Message Copied!';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
} catch (err) {
console.error('Failed to copy message: ', err);
btn.textContent = '❌ Failed';
btn.style.background = '#f44336';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
} finally {
document.body.removeChild(textArea);
}
}
}
// Save settings to cookies when form is submitted
const pushForm = document.getElementById('pushForm');
if (pushForm) {

625
web/style-input.css Normal file
View File

@@ -0,0 +1,625 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Import all custom CSS variables from style.css */
.bg-yellow-900 { background-color: #713f12; }
.bg-yellow-900\/20 { background-color: rgb(113 63 18 / 0.2); }
/* Gradient backgrounds */
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
.from-blue-600 { --tw-gradient-from: #2563eb; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgb(37 99 235 / 0)); }
.to-blue-700 { --tw-gradient-to: #1d4ed8; }
.hover\:from-blue-700:hover { --tw-gradient-from: #1d4ed8; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgb(29 78 216 / 0)); }
.hover\:to-blue-800:hover { --tw-gradient-to: #1e40af; }
/* Additional hover states for gradients */
.hover\:bg-green-600:hover { background-color: #16a34a; }
.hover\:bg-red-600:hover { background-color: #dc2626; }
/* Responsive font sizes */oot {
--bg-color: #0f172a;
--text-color: #e2e8f0;
--section-bg: #1e293b;
--border-color: #334155;
--highlight: #3b82f6;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--dark-bg: #1e293b;
--dark-text: #f8fafc;
--dark-muted: #94a3b8;
--dark-border: #475569;
}
/* Force inclusion of all classes used in the project */
@layer utilities {
/* Custom dark theme classes from style.css */
.bg-dark-bg { background-color: var(--dark-bg); }
.bg-dark-surface { background-color: var(--section-bg); }
.text-dark-text { color: var(--dark-text); }
.text-dark-muted { color: var(--dark-muted); }
.border-dark-border { border-color: var(--dark-border); }
/* Ensure all gray colors are included */
.bg-gray-50 { background-color: #f9fafb; }
.bg-gray-100 { background-color: #f3f4f6; }
.bg-gray-200 { background-color: #e5e7eb; }
.bg-gray-300 { background-color: #d1d5db; }
.bg-gray-400 { background-color: #9ca3af; }
.bg-gray-500 { background-color: #6b7280; }
.bg-gray-600 { background-color: #4b5563; }
.bg-gray-700 { background-color: #374151; }
.bg-gray-800 { background-color: #1f2937; }
.bg-gray-900 { background-color: #111827; }
.text-gray-50 { color: #f9fafb; }
.text-gray-100 { color: #f3f4f6; }
.text-gray-200 { color: #e5e7eb; }
.text-gray-300 { color: #d1d5db; }
.text-gray-400 { color: #9ca3af; }
.text-gray-500 { color: #6b7280; }
.text-gray-600 { color: #4b5563; }
.text-gray-700 { color: #374151; }
.text-gray-800 { color: #1f2937; }
.text-gray-900 { color: #111827; }
.border-gray-50 { border-color: #f9fafb; }
.border-gray-100 { border-color: #f3f4f6; }
.border-gray-200 { border-color: #e5e7eb; }
.border-gray-300 { border-color: #d1d5db; }
.border-gray-400 { border-color: #9ca3af; }
.border-gray-500 { border-color: #6b7280; }
.border-gray-600 { border-color: #4b5563; }
.border-gray-700 { border-color: #374151; }
.border-gray-800 { border-color: #1f2937; }
.border-gray-900 { border-color: #111827; }
/* Blue colors */
.bg-blue-100 { background-color: #dbeafe; }
.bg-blue-400 { background-color: #60a5fa; }
.bg-blue-500 { background-color: #3b82f6; }
.bg-blue-600 { background-color: #2563eb; }
.bg-blue-700 { background-color: #1d4ed8; }
.text-blue-100 { color: #dbeafe; }
.text-blue-200 { color: #bfdbfe; }
.text-blue-400 { color: #60a5fa; }
.text-blue-500 { color: #3b82f6; }
.text-blue-600 { color: #2563eb; }
.text-blue-700 { color: #1d4ed8; }
.border-blue-500 { border-color: #3b82f6; }
.border-blue-600 { border-color: #2563eb; }
/* Green colors */
.bg-green-500 { background-color: #22c55e; }
.bg-green-600 { background-color: #16a34a; }
.bg-green-700 { background-color: #15803d; }
.bg-green-900 { background-color: #14532d; }
.text-green-200 { color: #bbf7d0; }
.text-green-400 { color: #4ade80; }
.text-green-600 { color: #16a34a; }
.text-green-700 { color: #15803d; }
.border-green-500 { border-color: #22c55e; }
.border-green-600 { border-color: #16a34a; }
/* Red colors */
.bg-red-400 { background-color: #f87171; }
.bg-red-600 { background-color: #dc2626; }
.bg-red-700 { background-color: #b91c1c; }
.bg-red-900 { background-color: #7f1d1d; }
.text-red-100 { color: #fee2e2; }
.text-red-200 { color: #fecaca; }
.text-red-400 { color: #f87171; }
.text-red-600 { color: #dc2626; }
.text-red-700 { color: #b91c1c; }
.border-red-600 { border-color: #dc2626; }
/* Purple colors */
.bg-purple-600 { background-color: #9333ea; }
.bg-purple-700 { background-color: #7c3aed; }
.text-purple-400 { color: #c084fc; }
.border-purple-500 { border-color: #a855f7; }
/* Yellow colors */
.bg-yellow-600 { background-color: #ca8a04; }
.bg-yellow-700 { background-color: #a16207; }
.bg-yellow-900 { background-color: #713f12; }
.bg-yellow-900\/20 { background-color: rgb(113 63 18 / 0.2); }
.text-yellow-100 { color: #fef3c7; }
.text-yellow-200 { color: #fde68a; }
.text-yellow-400 { color: #fbbf24; }
.border-yellow-500 { border-color: #eab308; }
.border-yellow-600 { border-color: #ca8a04; }
/* White colors */
.bg-white { background-color: #ffffff; }
.text-white { color: #ffffff; }
/* Ring colors with opacity */
.ring-blue-500\/20 { --tw-ring-color: rgb(59 130 246 / 0.2); }
.focus\:ring-blue-500\/20:focus { --tw-ring-color: rgb(59 130 246 / 0.2); }
/* Background opacity */
.bg-opacity-20 { --tw-bg-opacity: 0.2; }
/* Text sizes */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-4xl { font-size: 2.25rem; line-height: 2.5rem; }
.text-5xl { font-size: 3rem; line-height: 1; }
.text-6xl { font-size: 3.75rem; line-height: 1; }
/* Layout and positioning */
.flex { display: flex; }
.flex-1 { flex: 1 1 0%; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-start { justify-content: flex-start; }
.space-x-2 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.5rem; }
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
/* Container */
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.mx-auto { margin-left: auto; margin-right: auto; }
/* Essential flexbox utilities */
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-shrink-0 { flex-shrink: 0; }
.items-stretch { align-items: stretch; }
/* Additional utility classes */
.text-center { text-align: center; }
.inline-block { display: inline-block; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
.pb-2 { padding-bottom: 0.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.uppercase { text-transform: uppercase; }
.tracking-wide { letter-spacing: 0.025em; }
.block { display: block; }
/* Sizing */
.min-h-screen { min-height: 100vh; }
.min-w-0 { min-width: 0; }
.max-w-lg { max-width: 32rem; }
.max-w-md { max-width: 28rem; }
.max-w-3xl { max-width: 48rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-6xl { max-width: 72rem; }
/* Positioning */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.static { position: static; }
.top-0 { top: 0; }
.top-4 { top: 1rem; }
.bottom-0 { bottom: 0; }
.left-0 { left: 0; }
.right-0 { right: 0; }
.right-2 { right: 0.5rem; }
.right-4 { right: 1rem; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-1\/2 { top: 50%; }
.-translate-y-1\/2 { transform: translateY(-50%); }
.left-1\/2 { left: 50%; }
.-translate-x-1\/2 { transform: translateX(-50%); }
/* Z-index */
.z-10 { z-index: 10; }
.z-40 { z-index: 40; }
.z-50 { z-index: 50; }
/* Responsive flexbox and grid */
@media (min-width: 640px) {
.sm\\:flex-row { flex-direction: row; }
.sm\\:items-center { align-items: center; }
}
/* Responsive grid for large screens */
@media (min-width: 1024px) {
.lg\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lg\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Essential missing classes */
.space-x-3 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.75rem; }
.focus\\:border-blue-500:focus { border-color: #3b82f6; }
.focus\\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.focus\\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
/* Font weights */
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", monospace; }
/* Spacing - margin */
.m-0 { margin: 0; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mb-16 { margin-bottom: 4rem; }
.mr-4 { margin-right: 1rem; }
.mt-1 { margin-top: 0.25rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
/* Spacing - padding */
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.p-12 { padding: 3rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.px-8 { padding-left: 2rem; padding-right: 2rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
/* Border radius */
.rounded { border-radius: 0.25rem; }
.rounded-md { border-radius: 0.375rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-3xl { border-radius: 1.5rem; }
.rounded-full { border-radius: 9999px; }
/* Width and height */
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-10 { width: 2.5rem; }
.w-12 { width: 3rem; }
.w-full { width: 100%; }
.h-4 { height: 1rem; }
.h-5 { height: 1.25rem; }
.h-6 { height: 1.5rem; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.h-full { height: 100%; }
.h-px { height: 1px; }
.min-h-screen { min-height: 100vh; }
/* Max width */
.max-w-lg { max-width: 32rem; }
.max-w-md { max-width: 28rem; }
.max-w-3xl { max-width: 48rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-6xl { max-width: 72rem; }
/* Flexbox */
.flex { display: flex; }
.flex-1 { flex: 1 1 0%; }
.flex-wrap { flex-wrap: wrap; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
/* Grid */
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
/* Responsive grid classes */
@media (min-width: 1024px) {
.lg\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lg\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 768px) {
.md\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Gap */
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
/* Spacing utilities */
.space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); }
.space-x-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.75rem * var(--tw-space-x-reverse)); margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); }
.space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); }
.space-y-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(1rem * var(--tw-space-y-reverse)); }
.space-y-12 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(3rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(3rem * var(--tw-space-y-reverse)); }
/* Shadows */
.shadow-2xl { --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); }
/* Display */
.block { display: block; }
.inline-block { display: inline-block; }
.hidden { display: none; }
/* Text alignment */
.text-center { text-align: center; }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
/* Inset positioning */
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-0 { top: 0; }
.right-0 { right: 0; }
.bottom-0 { bottom: 0; }
.left-0 { left: 0; }
.top-4 { top: 1rem; }
.right-4 { right: 1rem; }
.left-4 { left: 1rem; }
/* Z-index */
.z-40 { z-index: 40; }
.z-50 { z-index: 50; }
/* Top/bottom/left/right */
.top-4 { top: 1rem; }
.right-4 { right: 1rem; }
.top-1\/2 { top: 50%; }
.left-1\/2 { left: 50%; }
.right-2 { right: 0.5rem; }
/* Transform */
.transform { transform: var(--tw-transform); }
.-translate-x-1\/2 { --tw-translate-x: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
.-translate-y-1\/2 { --tw-translate-y: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
.hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
/* Transitions */
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.duration-200 { transition-duration: 200ms; }
.duration-300 { transition-duration: 300ms; }
/* Cursor */
.cursor-pointer { cursor: pointer; }
/* Focus states */
.focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
.focus\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
.focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); }
/* Missing classes from headeranalyzer.html */
/* Word breaking and text wrapping */
.break-all { word-break: break-all; }
.break-words { word-break: break-word; }
.whitespace-pre-wrap { white-space: pre-wrap; }
/* Overflow utilities */
.overflow-x-auto { overflow-x: auto; }
.overflow-hidden { overflow: hidden; }
/* Table utilities */
.divide-y { border-top-width: 0; }
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-gray-600 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
}
/* Additional width/height classes */
.w-8 { width: 2rem; }
.w-48 { width: 12rem; }
.h-8 { height: 2rem; }
/* Text alignment */
.text-left { text-align: left; }
/* Border sides */
.border-r { border-right-width: 1px; border-right-style: solid; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
/* Resize utilities */
.resize-y { resize: vertical; }
/* Placeholder colors */
.placeholder-gray-400::placeholder { color: #9ca3af; }
/* Focus ring with opacity */
.focus\:ring-blue-500\/20:focus {
--tw-ring-color: rgb(59 130 246 / 0.2);
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
/* Background opacity variants */
.bg-gray-700\/50 { background-color: rgb(55 65 81 / 0.5); }
.bg-red-900\/20 { background-color: rgb(127 29 29 / 0.2); }
.bg-yellow-900\/20 { background-color: rgb(113 63 18 / 0.2); }
/* Additional spacing */
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
/* Additional margin */
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
/* Inline flex */
.inline-flex { display: inline-flex; }
/* Additional text sizes */
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
/* Hover states */
.hover\:bg-gray-600:hover { background-color: #4b5563; }
.hover\:bg-blue-700:hover { background-color: #1d4ed8; }
.hover\:bg-green-700:hover { background-color: #15803d; }
.hover\:bg-red-700:hover { background-color: #b91c1c; }
.hover\:text-blue-400:hover { color: #60a5fa; }
.hover\:text-blue-300:hover { color: #93c5fd; }
.hover\:border-green-500:hover { border-color: #22c55e; }
.hover\:bg-gray-700\/50:hover { background-color: rgb(55 65 81 / 0.5); }
/* Responsive classes */
@media (min-width: 640px) {
.sm\:flex-row { flex-direction: row; }
.sm\:items-center { align-items: center; }
.sm\:flex { display: flex; }
.sm\:mb-0 { margin-bottom: 0; }
}
@media (min-width: 768px) {
.md\:text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.md\:text-6xl { font-size: 3.75rem; line-height: 1; }
.md\:text-2xl { font-size: 1.5rem; line-height: 2rem; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
}
/* Custom CSS variables and styles */
:root {
--bg-color: #0f172a;
--text-color: #e2e8f0;
--section-bg: #1e293b;
--border-color: #334155;
--highlight: #3b82f6;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--dark-bg: #1e293b;
--dark-text: #f8fafc;
--dark-muted: #94a3b8;
--dark-border: #475569;
}
/* Custom Tailwind theme extensions */
.bg-dark-bg { background-color: var(--dark-bg); }
.text-dark-text { color: var(--dark-text); }
.text-dark-muted { color: var(--dark-muted); }
.border-dark-border { border-color: var(--dark-border); }
/* Custom animations */
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}
.animate-slide-out-right {
animation: slideOutRight 0.3s ease-in forwards;
}
/* Backdrop blur for popup */
/* Backdrop filters */
.backdrop-blur-none { backdrop-filter: blur(0); }
.backdrop-blur-sm { backdrop-filter: blur(4px); }
.backdrop-blur { backdrop-filter: blur(8px); }
.backdrop-blur-md { backdrop-filter: blur(12px); }
.backdrop-blur-lg { backdrop-filter: blur(16px); }
.backdrop-blur-xl { backdrop-filter: blur(24px); }
.backdrop-blur-popup {
backdrop-filter: blur(8px);
}
/* Floating button styles */
.floating-nav {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Custom component styles that extend Tailwind */
.password-generator {
max-width: 56rem;
margin: 0 auto;
padding: 1.5rem;
}
.tab-buttons {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tab-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
border: 1px solid var(--border-color);
background-color: var(--section-bg);
color: var(--text-color);
cursor: pointer;
}
.tab-btn:hover {
transform: scale(1.05);
background-color: var(--highlight);
}
.tab-btn.active {
background-color: var(--highlight);
color: white;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

1755
web/style-tailwind.css Normal file

File diff suppressed because it is too large Load Diff