fix layout, add correct protocol for pwpusher as well as message for sender.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,4 +2,7 @@
|
|||||||
/go.sum
|
/go.sum
|
||||||
/ImageMagick
|
/ImageMagick
|
||||||
wordlist.txt
|
wordlist.txt
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
/app_build/README.md
|
||||||
|
/app_build/tailwindcss-linux-x64
|
||||||
35
README.md
35
README.md
@@ -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)
|
## 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.
|
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
|
### 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
|
- (Optional) [Python 3](https://www.python.org/) with Pillow for icon conversion
|
||||||
```go
|
```go
|
||||||
go mod init headeranalyzer
|
go mod init gonetkit
|
||||||
go mod tidy
|
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`.
|
- 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:
|
- 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
|
python web/convert_icon.py web/icon.png web/icon.ico
|
||||||
# using ImageMagick convertor
|
# using ImageMagick convertor
|
||||||
magick envelope.jpg -define icon:auto-resize=16,32,48,64 favicon.ico
|
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 web\icon.png -define icon:auto-resize=16,32,48,64 web\favicon.ico
|
||||||
ImageMagick\magick.exe identify web\favicon.ico
|
ImageMagick\magick.exe identify web\favicon.ico
|
||||||
# App Icon:
|
# App Icon:
|
||||||
@@ -37,19 +42,19 @@ go clean -cache
|
|||||||
#### Windows (from Windows PowerShell or Command Prompt):
|
#### Windows (from Windows PowerShell or Command Prompt):
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (set environment variables before the command)
|
# 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
|
```cmd
|
||||||
REM Command Prompt (set environment variables before the command)
|
REM Command Prompt (set environment variables before the command)
|
||||||
set GOOS=windows
|
set GOOS=windows
|
||||||
set GOARCH=amd64
|
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):
|
#### Linux/macOS (from Bash):
|
||||||
```sh
|
```sh
|
||||||
# Build 64-bit Linux executable
|
# 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.
|
- 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):
|
#### Windows (from Windows PowerShell or Command Prompt):
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (set environment variables before the command)
|
# 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
|
```cmd
|
||||||
REM Command Prompt (set environment variables before the command)
|
REM Command Prompt (set environment variables before the command)
|
||||||
set GOOS=windows
|
set GOOS=windows
|
||||||
set GOARCH=amd64
|
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.
|
- 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):
|
#### Linux/macOS (from Bash):
|
||||||
```sh
|
```sh
|
||||||
# Build 64-bit Linux executable
|
# 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.
|
- 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
|
### 3. Run the App
|
||||||
|
|
||||||
- Double-click or run from terminal:
|
- Double-click or run from terminal:
|
||||||
- On Windows: `headeranalyzer.exe`
|
- On Windows: `GoNetKit.exe`
|
||||||
- On Linux: `./headeranalyzer`
|
- On Linux: `./goNetKit`
|
||||||
- The app will start a web server (default: http://localhost:8080) and show a system tray icon.
|
- 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.
|
- Use the tray menu to open the web UI or quit the app.
|
||||||
|
|
||||||
### 4. Usage Notes
|
### 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 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:
|
- **Try go build without -ldflags:** Rarely, the `-ldflags` flag can interfere. Try building with and without it:
|
||||||
```powershell
|
```powershell
|
||||||
go build -o headeranalyzer.exe main.go
|
go build -o GoNetKit.exe main.go
|
||||||
go build -ldflags "-H=windowsgui" -o headeranalyzer.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`.
|
- **Try go generate:** If you use `go generate`, ensure it does not overwrite or remove `icon.syso`.
|
||||||
|
|
||||||
|
|||||||
71
app_build/build-tailwind.bat
Normal file
71
app_build/build-tailwind.bat
Normal 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
70
app_build/build-tailwind.sh
Executable 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
43
app_build/build.sh
Executable 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
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
/* Import Tailwind CSS */
|
|
||||||
@import url('https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css');
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #0f172a;
|
--bg-color: #0f172a;
|
||||||
--text-color: #e2e8f0;
|
--text-color: #e2e8f0;
|
||||||
@@ -22,6 +19,34 @@
|
|||||||
.text-dark-muted { color: var(--dark-muted); }
|
.text-dark-muted { color: var(--dark-muted); }
|
||||||
.border-dark-border { border-color: var(--dark-border); }
|
.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 Styles */
|
||||||
.password-generator {
|
.password-generator {
|
||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
21
main.go
21
main.go
@@ -16,11 +16,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/landingpage"
|
"gonetkit/landingpage"
|
||||||
"headeranalyzer/parser"
|
"gonetkit/parser"
|
||||||
"headeranalyzer/passwordgenerator"
|
"gonetkit/passwordgenerator"
|
||||||
"headeranalyzer/pwpusher"
|
"gonetkit/pwpusher"
|
||||||
"headeranalyzer/resolver"
|
"gonetkit/resolver"
|
||||||
|
|
||||||
"github.com/getlantern/systray"
|
"github.com/getlantern/systray"
|
||||||
)
|
)
|
||||||
@@ -174,6 +174,17 @@ func main() {
|
|||||||
w.Write(data)
|
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("/", landingHandler)
|
||||||
|
|
||||||
http.Handle("/analyze", indexHandler)
|
http.Handle("/analyze", indexHandler)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/resolver"
|
"gonetkit/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -73,6 +73,44 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Validate CSRF token
|
// Validate CSRF token
|
||||||
csrfToken := r.FormValue("csrf_token")
|
csrfToken := r.FormValue("csrf_token")
|
||||||
if !h.csrf.ValidateToken(csrfToken) {
|
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)
|
http.Redirect(w, r, "/?error="+url.QueryEscape("Invalid security token. Please try again."), http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -95,13 +133,26 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("DEBUG: Headers validated successfully")
|
log.Printf("DEBUG: Headers validated successfully")
|
||||||
report := Analyze(validatedHeaders)
|
report := Analyze(validatedHeaders)
|
||||||
log.Printf("DEBUG: Analysis completed, From field: '%s'", report.From)
|
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 {
|
data := struct {
|
||||||
*Report
|
*Report
|
||||||
CurrentPage string
|
CurrentPage string
|
||||||
|
CSRFToken string
|
||||||
|
OriginalHeaders string
|
||||||
}{
|
}{
|
||||||
Report: report,
|
Report: report,
|
||||||
CurrentPage: "home",
|
CurrentPage: "home",
|
||||||
|
CSRFToken: freshCSRFToken,
|
||||||
|
OriginalHeaders: validatedHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("DEBUG: About to render template with data")
|
log.Printf("DEBUG: About to render template with data")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validator = security.NewInputValidator()
|
var validator = security.NewInputValidator()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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
|
// Prepare response URL with new format
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
|
|
||||||
|
// Check for HTTPS in multiple ways
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
scheme = "https"
|
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)
|
fullURL := fmt.Sprintf("%s://%s/s/%s?k=%s", scheme, r.Host, id, additionalKey)
|
||||||
|
|
||||||
// Save to user's history if tracking is enabled
|
// Save to user's history if tracking is enabled
|
||||||
@@ -632,6 +641,7 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
|||||||
"PushURL": fullURL,
|
"PushURL": fullURL,
|
||||||
"ID": id,
|
"ID": id,
|
||||||
"ExpiresAt": expiresAt.Format("2006-01-02 15:04:05"),
|
"ExpiresAt": expiresAt.Format("2006-01-02 15:04:05"),
|
||||||
|
"MaxViews": req.MaxViews,
|
||||||
"CurrentPage": "pwpush",
|
"CurrentPage": "pwpush",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headeranalyzer/security"
|
"gonetkit/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
|||||||
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./web/*.{html,js}",
|
||||||
|
"./**/*.go"
|
||||||
|
],
|
||||||
|
safelist: [],
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
146
web/base.html
146
web/base.html
@@ -4,62 +4,35 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{block "title" .}}{{end}}</title>
|
<title>{{block "title" .}}{{end}}</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<link rel="stylesheet" href="/style-tailwind.css">
|
||||||
<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>
|
|
||||||
<style>
|
<style>
|
||||||
/* Custom animations and styles */
|
/* Prevent horizontal overflow */
|
||||||
@keyframes slideInRight {
|
* {
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
box-sizing: border-box;
|
||||||
to { transform: translateX(0); opacity: 1; }
|
}
|
||||||
|
html, body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100vw;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideOutRight {
|
/* Ensure proper container widths */
|
||||||
from { transform: translateX(0); opacity: 1; }
|
.container {
|
||||||
to { transform: translateX(100%); opacity: 0; }
|
max-width: 100vw;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-right {
|
main {
|
||||||
animation: slideInRight 0.3s ease-out forwards;
|
max-width: 100vw;
|
||||||
}
|
width: 100%;
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||||
{{block "head" .}}{{end}}
|
{{block "head" .}}{{end}}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-dark-text min-h-screen">
|
<body class="bg-dark-bg text-dark-text min-h-screen">
|
||||||
<!-- Floating Navigation -->
|
<!-- 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">
|
<div class="flex bg-dark-surface border border-dark-border rounded-full overflow-hidden">
|
||||||
<!-- Home Button -->
|
<!-- 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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Popup -->
|
<!-- 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 -->
|
<!-- 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 -->
|
<!-- 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="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">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-xl font-bold text-dark-text">Navigation</h2>
|
<h2 class="text-xl font-bold text-dark-text">Navigation</h2>
|
||||||
<button onclick="closeNavigationPopup()" class="text-dark-muted hover:text-dark-text transition-colors">
|
<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-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- IT Tools Category -->
|
<!-- IT Tools Category -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h3 class="text-sm font-semibold text-blue-400 uppercase tracking-wide mb-3">IT Tools</h3>
|
<h3 class="text-sm font-semibold text-blue-400 uppercase tracking-wide mb-3">IT Tools</h3>
|
||||||
<div class="space-y-2">
|
<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">
|
<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">
|
<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="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"/>
|
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
|
||||||
@@ -111,7 +84,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/dns" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
@@ -126,10 +99,10 @@
|
|||||||
|
|
||||||
<!-- Online Security Category -->
|
<!-- Online Security Category -->
|
||||||
<div>
|
<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">
|
<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">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
@@ -141,7 +114,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/pwpush" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
@@ -172,18 +145,63 @@
|
|||||||
// Navigation popup functionality
|
// Navigation popup functionality
|
||||||
function openNavigationPopup() {
|
function openNavigationPopup() {
|
||||||
const popup = document.getElementById('navigationPopup');
|
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() {
|
function closeNavigationPopup() {
|
||||||
const popup = document.getElementById('navigationPopup');
|
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
|
// Menu button click handler
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const menuButton = document.getElementById('menuButton');
|
const menuButton = document.getElementById('menuButton');
|
||||||
menuButton.addEventListener('click', openNavigationPopup);
|
menuButton.addEventListener('click', toggleNavigationPopup);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Popup notification system with Tailwind classes
|
// Popup notification system with Tailwind classes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{template "base.html" .}}
|
{{template "base.html" .}}
|
||||||
|
|
||||||
{{define "title"}}DNS Tools - HeaderAnalyzer{{end}}
|
{{define "title"}}DNS Tools{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
|
|||||||
224
web/dns.html.old
224
web/dns.html.old
@@ -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()">×</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}}
|
|
||||||
@@ -40,11 +40,17 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .From}}
|
{{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">
|
<div id="report" class="space-y-6">
|
||||||
<!-- Sender Identification -->
|
<!-- Sender Identification -->
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<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>
|
<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 class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-medium text-gray-400">Envelope Sender (Return-Path):</span>
|
<span class="text-sm font-medium text-gray-400">Envelope Sender (Return-Path):</span>
|
||||||
@@ -161,7 +167,7 @@
|
|||||||
<!-- Basic Information -->
|
<!-- Basic Information -->
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<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>
|
<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 class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-medium text-gray-400">From:</span>
|
<span class="text-sm font-medium text-gray-400">From:</span>
|
||||||
@@ -246,7 +252,7 @@
|
|||||||
<!-- Security Analysis -->
|
<!-- Security Analysis -->
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<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>
|
<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 -->
|
<!-- SPF Authentication -->
|
||||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
<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>
|
<h3 class="text-lg font-semibold text-gray-200 mb-3">SPF Authentication</h3>
|
||||||
@@ -595,10 +601,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportImage() {
|
function exportImage() {
|
||||||
if (typeof html2canvas === 'undefined') {
|
// if (typeof html2canvas === 'undefined') {
|
||||||
alert('Image export library not loaded. Please refresh the page and try again.');
|
// alert('Image export library not loaded. Please refresh the page and try again.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
html2canvas(document.querySelector("#report")).then(canvas => {
|
html2canvas(document.querySelector("#report")).then(canvas => {
|
||||||
let link = document.createElement("a");
|
let link = document.createElement("a");
|
||||||
@@ -639,19 +645,19 @@
|
|||||||
|
|
||||||
function exportPDF() {
|
function exportPDF() {
|
||||||
// Check if libraries are available
|
// Check if libraries are available
|
||||||
if (typeof html2canvas === 'undefined') {
|
// if (typeof html2canvas === 'undefined') {
|
||||||
alert('HTML2Canvas library not loaded. Please refresh the page and try again.');
|
// alert('HTML2Canvas library not loaded. Please refresh the page and try again.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check if we have either html2pdf or jsPDF available
|
// Check if we have either html2pdf or jsPDF available
|
||||||
const hasHtml2pdf = typeof html2pdf !== 'undefined';
|
const hasHtml2pdf = typeof html2pdf !== 'undefined';
|
||||||
const hasJsPDF = typeof window.jsPDF !== 'undefined';
|
const hasJsPDF = typeof window.jsPDF !== 'undefined';
|
||||||
|
|
||||||
if (!hasHtml2pdf && !hasJsPDF) {
|
// if (!hasHtml2pdf && !hasJsPDF) {
|
||||||
alert('PDF generation library not loaded. Please refresh the page and try again.');
|
// alert('PDF generation library not loaded. Please refresh the page and try again.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Store original states
|
// Store original states
|
||||||
const originalDetailsStates = {};
|
const originalDetailsStates = {};
|
||||||
@@ -903,7 +909,6 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PDF export error:', error);
|
console.error('PDF export error:', error);
|
||||||
alert('PDF export failed. Please refresh the page and try again.');
|
|
||||||
restoreOriginalState();
|
restoreOriginalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{template "base.html" .}}
|
{{template "base.html" .}}
|
||||||
|
|
||||||
{{define "title"}}HeaderAnalyzer - IT & Security Tools{{end}}
|
{{define "title"}}GoNetKit{{end}}
|
||||||
|
|
||||||
{{define "head"}}
|
{{define "head"}}
|
||||||
<style>
|
<style>
|
||||||
@@ -24,19 +24,19 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="min-h-screen">
|
<div>
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="text-center mb-16">
|
<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>
|
<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">
|
<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
|
Your comprehensive toolkit for IT diagnostics and online security
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap justify-center gap-4 text-sm text-blue-200">
|
<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/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/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">Data Protection</span>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<!-- Online Security Category -->
|
<!-- Online Security Category -->
|
||||||
<div class="category-section">
|
<div class="category-section">
|
||||||
<div class="flex items-center mb-8">
|
<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
|
ONLINE SECURITY
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 h-px bg-dark-border"></div>
|
<div class="flex-1 h-px bg-dark-border"></div>
|
||||||
|
|||||||
@@ -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}}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{{template "base.html" .}}
|
{{template "base.html" .}}
|
||||||
|
|
||||||
{{define "title"}}Password Generator - HeaderAnalyzer{{end}}
|
{{define "title"}}Password Generator{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-6">
|
||||||
<a href="/pwgenerator" class="inline-block">
|
<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
|
🔐 Password Generator
|
||||||
</h1>
|
</h1>
|
||||||
</a>
|
</a>
|
||||||
@@ -15,61 +15,59 @@
|
|||||||
<input type="hidden" id="csrfToken" value="{{.CSRFToken}}">
|
<input type="hidden" id="csrfToken" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
<!-- Tab Buttons -->
|
<!-- Tab Buttons -->
|
||||||
<div class="flex space-x-2 mb-6 bg-gray-800 p-2 rounded-lg border border-gray-700">
|
<div class="flex space-x-2 mb-4 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">
|
<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
|
🎲 Random Password
|
||||||
</button>
|
</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
|
📝 Passphrase
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password Output -->
|
<!-- Password Output -->
|
||||||
<!-- Generated Password Display -->
|
<!-- Generated Password Display -->
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
|
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-6">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
<div class="flex items-center justify-between gap-3 mb-3">
|
||||||
<h2 class="text-xl font-semibold text-gray-200">🔐 Generated Password</h2>
|
<h2 class="text-lg 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 text-sm flex-shrink-0">
|
||||||
<span id="characterCount" class="text-gray-400 bg-gray-700 px-3 py-1 rounded-full">
|
0 characters
|
||||||
0 characters
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="text" id="generatedPassword" readonly
|
<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">
|
placeholder="Click 'Generate Password' to create a new password">
|
||||||
<button onclick="copyPassword()"
|
<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
|
📋 Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Generate Button -->
|
<!-- 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()"
|
<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
|
🎲 Generate Password
|
||||||
</button>
|
</button>
|
||||||
<button onclick="copyCurrentURL()"
|
<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
|
🔗 Copy URL with Settings
|
||||||
</button>
|
</button>
|
||||||
<button onclick="resetAllSettings()"
|
<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
|
🔄 Reset
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
|
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-6">
|
||||||
<div class="flex items-center justify-between mb-4 cursor-pointer" onclick="toggleSettings()">
|
<div class="flex items-center justify-between mb-3 cursor-pointer" onclick="toggleSettings()">
|
||||||
<h3 class="text-xl font-semibold text-gray-200">🔧 Password Settings</h3>
|
<h3 class="text-lg font-semibold text-gray-200">🔧 Password Settings</h3>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span id="settingsToggleText" class="text-sm text-gray-400">Click to expand</span>
|
<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"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +75,7 @@
|
|||||||
|
|
||||||
<div id="settingsContent" class="hidden">
|
<div id="settingsContent" class="hidden">
|
||||||
<!-- Save Passwords Option -->
|
<!-- 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">
|
<div class="flex items-center space-x-3">
|
||||||
<input type="checkbox" id="savePasswords" onchange="togglePasswordSaving(); autoSaveSettings()"
|
<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">
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Table -->
|
<!-- 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 -->
|
<!-- Left Column: Basic Password Settings -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<h4 class="text-lg font-medium text-gray-200 mb-4 border-b border-gray-600 pb-2">🎲 Random Password Settings</h4>
|
<h4 class="text-base font-medium text-gray-200 mb-2 border-b border-gray-600 pb-1">🎲 Random Password Settings</h4>
|
||||||
|
|
||||||
<div>
|
<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()"
|
<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>
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
@@ -273,12 +271,12 @@ function toggleSettings() {
|
|||||||
if (content.classList.contains('hidden')) {
|
if (content.classList.contains('hidden')) {
|
||||||
// Expand
|
// Expand
|
||||||
content.classList.remove('hidden');
|
content.classList.remove('hidden');
|
||||||
chevron.style.transform = 'rotate(180deg)';
|
chevron.classList.add('rotate-180');
|
||||||
toggleText.textContent = 'Click to minimize';
|
toggleText.textContent = 'Click to minimize';
|
||||||
} else {
|
} else {
|
||||||
// Collapse
|
// Collapse
|
||||||
content.classList.add('hidden');
|
content.classList.add('hidden');
|
||||||
chevron.style.transform = 'rotate(0deg)';
|
chevron.classList.remove('rotate-180');
|
||||||
toggleText.textContent = 'Click to expand';
|
toggleText.textContent = 'Click to expand';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,11 +486,11 @@ function loadSavePasswordsSetting() {
|
|||||||
function showConfirmationPopup(title, message, onConfirm) {
|
function showConfirmationPopup(title, message, onConfirm) {
|
||||||
// Create backdrop
|
// Create backdrop
|
||||||
const backdrop = document.createElement('div');
|
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
|
// Create popup
|
||||||
backdrop.innerHTML = `
|
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">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-xl font-bold text-red-400">${title}</h3>
|
<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">
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-200 transition-colors">
|
||||||
|
|||||||
144
web/pwpush.html
144
web/pwpush.html
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{{define "head"}}
|
{{define "head"}}
|
||||||
{{if .Success}}
|
{{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}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@@ -32,12 +32,18 @@
|
|||||||
readonly
|
readonly
|
||||||
onclick="this.select()"
|
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">
|
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()"
|
<div class="flex flex-row items-stretch justify-center gap-3">
|
||||||
class="copy-btn bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
|
<button onclick="copyToClipboard()"
|
||||||
📋 Copy
|
class="copy-btn bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors whitespace-nowrap">
|
||||||
</button>
|
📋 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>
|
</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"
|
<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">
|
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
|
Create Another Link
|
||||||
@@ -95,7 +101,8 @@
|
|||||||
id="expiry_days"
|
id="expiry_days"
|
||||||
min="1" max="90" value="7"
|
min="1" max="90" value="7"
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
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">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span id="expiryDisplay" class="font-bold text-blue-400">7 days</span>
|
<span id="expiryDisplay" class="font-bold text-blue-400">7 days</span>
|
||||||
<small class="text-gray-500">Max: 3 months</small>
|
<small class="text-gray-500">Max: 3 months</small>
|
||||||
@@ -109,7 +116,8 @@
|
|||||||
id="max_views"
|
id="max_views"
|
||||||
min="1" max="100" value="10"
|
min="1" max="100" value="10"
|
||||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
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">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span id="viewsDisplay" class="font-bold text-blue-400">10 views</span>
|
<span id="viewsDisplay" class="font-bold text-blue-400">10 views</span>
|
||||||
<small class="text-gray-500">Max: 100 views</small>
|
<small class="text-gray-500">Max: 100 views</small>
|
||||||
@@ -124,7 +132,8 @@
|
|||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="require_click"
|
name="require_click"
|
||||||
checked
|
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>
|
<span class="text-sm font-medium text-gray-200">🛡️ Require click to reveal</span>
|
||||||
</label>
|
</label>
|
||||||
<small class="block text-xs text-gray-400 ml-8">Hides content from web crawlers and requires user interaction</small>
|
<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">
|
<label class="flex items-center space-x-3 cursor-pointer">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="auto_delete"
|
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>
|
<span class="text-sm font-medium text-gray-200">🗑️ Allow manual deletion</span>
|
||||||
</label>
|
</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>
|
<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"
|
<input type="checkbox"
|
||||||
name="track_history"
|
name="track_history"
|
||||||
id="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>
|
<span class="text-sm font-medium text-gray-200">📚 Save to my history</span>
|
||||||
</label>
|
</label>
|
||||||
<small class="block text-xs text-gray-400 ml-8">Keep a record of your created links (stored in browser)</small>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="text-center pt-6 border-t border-gray-700">
|
<div class="text-center pt-6">
|
||||||
<button type="submit"
|
<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">
|
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
|
🔒 Create Secure Link
|
||||||
@@ -290,6 +301,30 @@ function updateViewsDisplay(views) {
|
|||||||
display.textContent = views + (views === '1' ? ' view' : ' 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() {
|
function copyToClipboard() {
|
||||||
const urlInput = document.getElementById('pushUrl');
|
const urlInput = document.getElementById('pushUrl');
|
||||||
const btn = document.querySelector('.copy-btn');
|
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
|
// Save settings to cookies when form is submitted
|
||||||
const pushForm = document.getElementById('pushForm');
|
const pushForm = document.getElementById('pushForm');
|
||||||
if (pushForm) {
|
if (pushForm) {
|
||||||
|
|||||||
625
web/style-input.css
Normal file
625
web/style-input.css
Normal 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
1755
web/style-tailwind.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user