revamped CSS - using Tailwind now, update layout and added home page
This commit is contained in:
7550
example.txt
7550
example.txt
File diff suppressed because it is too large
Load Diff
BIN
headeranalyzer
BIN
headeranalyzer
Binary file not shown.
42
landingpage/handler.go
Normal file
42
landingpage/handler.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package landingpage
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
func NewHandler(embeddedFS fs.FS) (*Handler, error) {
|
||||
tmpl, err := template.ParseFS(embeddedFS, "web/base.html", "web/landing_page.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
template: tmpl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Only handle root path
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
CurrentPage string
|
||||
}{
|
||||
CurrentPage: "home",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.template.ExecuteTemplate(w, "base.html", data); err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
26
main.go
26
main.go
@@ -16,6 +16,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"headeranalyzer/landingpage"
|
||||
"headeranalyzer/parser"
|
||||
"headeranalyzer/passwordgenerator"
|
||||
"headeranalyzer/pwpusher"
|
||||
@@ -129,6 +130,10 @@ func main() {
|
||||
passwordgenerator.InitWordList()
|
||||
|
||||
// Create handlers with separate template sets
|
||||
landingHandler, err := landingpage.NewHandler(embeddedFiles)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize landing page handler: %v", err)
|
||||
}
|
||||
indexHandler := parser.NewHandler(embeddedFiles)
|
||||
dnsHandler := resolver.NewHandler(embeddedFiles)
|
||||
passwordHandler := passwordgenerator.NewHandler(embeddedFiles)
|
||||
@@ -158,17 +163,30 @@ func main() {
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
http.Handle("/", indexHandler)
|
||||
// Serve CSS file with correct MIME type
|
||||
http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := fs.ReadFile(staticFS, "style.css")
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
http.Handle("/", landingHandler)
|
||||
|
||||
http.Handle("/analyze", indexHandler)
|
||||
|
||||
http.Handle("/dns", dnsHandler)
|
||||
|
||||
http.HandleFunc("/api/dns", resolver.DNSAPIHandler)
|
||||
|
||||
http.Handle("/password", passwordHandler)
|
||||
http.Handle("/pwgenerator", passwordHandler)
|
||||
|
||||
http.HandleFunc("/api/password", passwordgenerator.PasswordAPIHandler)
|
||||
http.HandleFunc("/api/pwgenerator", passwordgenerator.PasswordAPIHandler)
|
||||
|
||||
http.HandleFunc("/api/password/info", passwordgenerator.PasswordInfoAPIHandler)
|
||||
http.HandleFunc("/api/pwgenerator/info", passwordgenerator.PasswordInfoAPIHandler)
|
||||
|
||||
// Register PWPusher routes
|
||||
pwPusher.RegisterRoutesWithDefault()
|
||||
|
||||
@@ -3,6 +3,7 @@ package passwordgenerator
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"headeranalyzer/security"
|
||||
)
|
||||
@@ -23,6 +24,7 @@ func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
IncludeLower bool `json:"includeLower"`
|
||||
NumberCount int `json:"numberCount"`
|
||||
SpecialChars string `json:"specialChars"`
|
||||
MinSpecialChars int `json:"minSpecialChars"`
|
||||
NoConsecutive bool `json:"noConsecutive"`
|
||||
WordCount int `json:"wordCount"`
|
||||
NumberPosition string `json:"numberPosition"`
|
||||
@@ -49,6 +51,12 @@ func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if requestData.MinSpecialChars < 0 || requestData.MinSpecialChars > 20 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Minimum special characters count must be between 0 and 20"))
|
||||
return
|
||||
}
|
||||
|
||||
if requestData.WordCount < 2 || requestData.WordCount > 10 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Word count must be between 2 and 10"))
|
||||
@@ -72,8 +80,17 @@ func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
requestData.NumberPosition = "end" // Default
|
||||
}
|
||||
|
||||
// Sanitize special characters to prevent potential issues
|
||||
requestData.SpecialChars = validator.SanitizeHTML(requestData.SpecialChars)
|
||||
// Validate special characters - only allow specific safe characters
|
||||
if len(requestData.SpecialChars) > 0 {
|
||||
allowedSpecialChars := "!@#$%&*-_=+."
|
||||
for _, char := range requestData.SpecialChars {
|
||||
if !strings.ContainsRune(allowedSpecialChars, char) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Special characters must only contain: !@#$%&*-_=+."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to internal Config format
|
||||
config := Config{
|
||||
@@ -82,6 +99,7 @@ func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
IncludeLower: requestData.IncludeLower,
|
||||
NumberCount: requestData.NumberCount,
|
||||
SpecialChars: requestData.SpecialChars,
|
||||
MinSpecialChars: requestData.MinSpecialChars,
|
||||
NoConsecutive: requestData.NoConsecutive,
|
||||
UsePassphrase: requestData.Type == "passphrase",
|
||||
WordCount: requestData.WordCount,
|
||||
|
||||
@@ -18,6 +18,7 @@ type Config struct {
|
||||
IncludeLower bool `json:"includeLower"`
|
||||
NumberCount int `json:"numberCount"`
|
||||
SpecialChars string `json:"specialChars"`
|
||||
MinSpecialChars int `json:"minSpecialChars"`
|
||||
NoConsecutive bool `json:"noConsecutive"`
|
||||
UsePassphrase bool `json:"usePassphrase"`
|
||||
WordCount int `json:"wordCount"`
|
||||
@@ -232,8 +233,9 @@ func DefaultConfig() Config {
|
||||
IncludeUpper: true,
|
||||
IncludeLower: true,
|
||||
NumberCount: 1,
|
||||
SpecialChars: "!@#$%^&*-_=+",
|
||||
NoConsecutive: false,
|
||||
SpecialChars: "!@#$%&*-_=+.",
|
||||
MinSpecialChars: 3,
|
||||
NoConsecutive: true,
|
||||
UsePassphrase: true, // Default to passphrase
|
||||
WordCount: 3,
|
||||
NumberPosition: "end",
|
||||
@@ -279,6 +281,18 @@ func generateRandomPassword(config Config) (string, error) {
|
||||
return "", fmt.Errorf("no character types selected")
|
||||
}
|
||||
|
||||
// Validate that minimum special characters doesn't exceed password length
|
||||
totalRequired := config.NumberCount + config.MinSpecialChars
|
||||
if config.IncludeLower {
|
||||
totalRequired++
|
||||
}
|
||||
if config.IncludeUpper {
|
||||
totalRequired++
|
||||
}
|
||||
if totalRequired > config.Length {
|
||||
return "", fmt.Errorf("password length too short for required character counts")
|
||||
}
|
||||
|
||||
password := make([]byte, config.Length)
|
||||
|
||||
// Ensure at least one character from each required set
|
||||
@@ -304,10 +318,50 @@ func generateRandomPassword(config Config) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Then place at least one from each other required character set
|
||||
// Place required special characters
|
||||
specialCharsPlaced := 0
|
||||
if config.MinSpecialChars > 0 && len(config.SpecialChars) > 0 {
|
||||
attempts := 0
|
||||
maxAttempts := config.Length * 10 // Prevent infinite loops
|
||||
for specialCharsPlaced < config.MinSpecialChars && attempts < maxAttempts {
|
||||
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
posInt := int(pos.Int64())
|
||||
|
||||
if !usedPositions[posInt] {
|
||||
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[posInt] = config.SpecialChars[char.Int64()]
|
||||
usedPositions[posInt] = true
|
||||
specialCharsPlaced++
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
// If we couldn't place enough special characters due to bad luck, force placement
|
||||
if specialCharsPlaced < config.MinSpecialChars {
|
||||
for i := 0; i < config.Length && specialCharsPlaced < config.MinSpecialChars; i++ {
|
||||
if !usedPositions[i] {
|
||||
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[i] = config.SpecialChars[char.Int64()]
|
||||
usedPositions[i] = true
|
||||
specialCharsPlaced++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then place at least one from each other required character set (excluding numbers and special chars already handled)
|
||||
for _, reqSet := range required {
|
||||
if reqSet == "0123456789" {
|
||||
continue // Already handled numbers
|
||||
if reqSet == "0123456789" || reqSet == config.SpecialChars {
|
||||
continue // Already handled numbers and special chars
|
||||
}
|
||||
|
||||
placed := false
|
||||
@@ -344,11 +398,60 @@ func generateRandomPassword(config Config) (string, error) {
|
||||
}
|
||||
|
||||
// Handle no consecutive characters requirement
|
||||
finalPassword := string(password)
|
||||
if config.NoConsecutive {
|
||||
return ensureNoConsecutive(string(password), charset)
|
||||
finalPassword, err := ensureNoConsecutive(string(password), charset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(password), nil
|
||||
// After ensureNoConsecutive, we need to verify minimum requirements are still met
|
||||
// and fix them if necessary
|
||||
if config.MinSpecialChars > 0 && len(config.SpecialChars) > 0 {
|
||||
// Count current special characters
|
||||
currentSpecialCount := 0
|
||||
for _, char := range finalPassword {
|
||||
if strings.ContainsRune(config.SpecialChars, char) {
|
||||
currentSpecialCount++
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have enough special characters, replace some non-special characters
|
||||
if currentSpecialCount < config.MinSpecialChars {
|
||||
passwordRunes := []rune(finalPassword)
|
||||
needed := config.MinSpecialChars - currentSpecialCount
|
||||
|
||||
for i := 0; i < len(passwordRunes) && needed > 0; i++ {
|
||||
// Find non-special characters that can be replaced
|
||||
if !strings.ContainsRune(config.SpecialChars, passwordRunes[i]) {
|
||||
// Make sure this replacement won't create consecutive characters
|
||||
specialChar, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newChar := rune(config.SpecialChars[specialChar.Int64()])
|
||||
|
||||
// Check if this would create consecutive characters
|
||||
wouldCreateConsecutive := false
|
||||
if i > 0 && passwordRunes[i-1] == newChar {
|
||||
wouldCreateConsecutive = true
|
||||
}
|
||||
if i < len(passwordRunes)-1 && passwordRunes[i+1] == newChar {
|
||||
wouldCreateConsecutive = true
|
||||
}
|
||||
|
||||
if !wouldCreateConsecutive {
|
||||
passwordRunes[i] = newChar
|
||||
needed--
|
||||
}
|
||||
}
|
||||
}
|
||||
finalPassword = string(passwordRunes)
|
||||
}
|
||||
}
|
||||
return finalPassword, nil
|
||||
}
|
||||
return finalPassword, nil
|
||||
}
|
||||
|
||||
func generatePassphrase(config Config) (string, error) {
|
||||
|
||||
@@ -25,12 +25,12 @@ type PasswordConfig struct {
|
||||
IncludeLower bool
|
||||
NumberCount int
|
||||
SpecialChars string
|
||||
MinSpecialChars int
|
||||
NoConsecutive bool
|
||||
WordCount int
|
||||
UseNumbers bool
|
||||
UseSpecial bool
|
||||
NumberPosition string
|
||||
SavePasswords bool
|
||||
}
|
||||
|
||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||
@@ -59,7 +59,7 @@ func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
}).ParseFS(embeddedFiles, "web/base.html", "web/password.html"))
|
||||
}).ParseFS(embeddedFiles, "web/base.html", "web/pwgenerator.html"))
|
||||
|
||||
return &Handler{
|
||||
templates: tmpl,
|
||||
@@ -72,7 +72,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Generate CSRF token
|
||||
csrfToken, err := h.csrf.GenerateToken()
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/password?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/pwgenerator?error="+url.QueryEscape("Security token generation failed"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,14 +82,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Length: getIntParam(r, "length", 12),
|
||||
IncludeUpper: getBoolParam(r, "includeUpper", true),
|
||||
IncludeLower: getBoolParam(r, "includeLower", true),
|
||||
NumberCount: getIntParam(r, "numberCount", 1),
|
||||
SpecialChars: getStringParam(r, "specialChars", "!@#$%^&*-_=+"),
|
||||
NoConsecutive: getBoolParam(r, "noConsecutive", false),
|
||||
NumberCount: getIntParam(r, "numberCount", 2),
|
||||
SpecialChars: getStringParam(r, "specialChars", "!@#$%&*-_=+."),
|
||||
MinSpecialChars: getIntParam(r, "minSpecialChars", 3),
|
||||
NoConsecutive: getBoolParam(r, "noConsecutive", true),
|
||||
WordCount: getIntParam(r, "wordCount", 3),
|
||||
UseNumbers: getBoolParam(r, "useNumbers", true),
|
||||
UseSpecial: getBoolParam(r, "useSpecial", false),
|
||||
NumberPosition: getStringParam(r, "numberPosition", "end"),
|
||||
SavePasswords: getBoolParam(r, "savePasswords", false),
|
||||
}
|
||||
|
||||
data := struct {
|
||||
@@ -101,7 +101,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Config: config,
|
||||
CSRFToken: csrfToken,
|
||||
}
|
||||
h.templates.ExecuteTemplate(w, "password.html", data)
|
||||
h.templates.ExecuteTemplate(w, "pwgenerator.html", data)
|
||||
}
|
||||
|
||||
// Helper functions to parse URL parameters
|
||||
|
||||
@@ -198,8 +198,6 @@ func initDatabase() (*sql.DB, error) {
|
||||
}
|
||||
|
||||
func loadPushTemplates(embeddedFS fs.FS) (*template.Template, error) {
|
||||
log.Printf("Loading PWPusher push templates...")
|
||||
|
||||
templates := template.New("").Funcs(template.FuncMap{
|
||||
"formatTime": func(t time.Time) string {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
@@ -214,14 +212,10 @@ func loadPushTemplates(embeddedFS fs.FS) (*template.Template, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse push templates: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("PWPusher push templates loaded successfully")
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func loadViewTemplates(embeddedFS fs.FS) (*template.Template, error) {
|
||||
log.Printf("Loading PWPusher view templates...")
|
||||
|
||||
templates := template.New("").Funcs(template.FuncMap{
|
||||
"formatTime": func(t time.Time) string {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
@@ -257,8 +251,6 @@ func loadViewTemplates(embeddedFS fs.FS) (*template.Template, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse view templates: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("PWPusher view templates loaded successfully")
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
@@ -442,9 +434,6 @@ func (p *PWPusher) recordFailedAttempt(clientIP string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Log for debugging
|
||||
log.Printf("Failed attempt recorded for IP %s: Count=%d, BlockedUntil=%v",
|
||||
clientIP, p.failedAttempts[clientIP].Count, p.failedAttempts[clientIP].BlockedUntil)
|
||||
}
|
||||
|
||||
func (p *PWPusher) resetFailedAttempts(clientIP string) {
|
||||
@@ -468,7 +457,6 @@ func (p *PWPusher) getBlockedUntil(clientIP string) time.Time {
|
||||
// HTTP Handlers
|
||||
|
||||
func (p *PWPusher) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("PWPusher IndexHandler called: %s %s", r.Method, r.URL.Path)
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
p.handleCreatePush(w, r)
|
||||
@@ -478,7 +466,6 @@ func (p *PWPusher) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// For GET requests, render the form with CurrentPage
|
||||
csrfToken, err := p.generateCSRFToken()
|
||||
if err != nil {
|
||||
log.Printf("Error generating CSRF token: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -495,7 +482,6 @@ func (p *PWPusher) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Success: false,
|
||||
CSRFToken: csrfToken,
|
||||
}
|
||||
log.Printf("Rendering pwpush.html template with data: %+v", data)
|
||||
p.renderTemplate(w, "pwpush.html", data)
|
||||
}
|
||||
|
||||
@@ -522,7 +508,8 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate CSRF token for form submissions
|
||||
csrfToken := r.FormValue("csrf_token")
|
||||
if !p.validateCSRFToken(csrfToken) {
|
||||
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||
w.Header().Set("Location", "/pwpusher?error="+url.QueryEscape("Invalid CSRF token"))
|
||||
http.Redirect(w, r, "/pwpusher?error="+url.QueryEscape("Invalid CSRF token"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -549,23 +536,30 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
||||
// Comprehensive input validation
|
||||
sanitizedText, err := p.validator.ValidateAndSanitizeText(req.Text, 100000)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
w.Header().Set("Location", "/pwpusher?error="+url.QueryEscape(err.Error()))
|
||||
http.Redirect(w, r, "/pwpusher?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
req.Text = sanitizedText
|
||||
|
||||
// Only validate password if one is provided (passwords are optional)
|
||||
if req.Password != "" {
|
||||
if err := p.validator.ValidatePassword(req.Password); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
w.Header().Set("Location", "/pwpusher?error="+url.QueryEscape(err.Error()))
|
||||
http.Redirect(w, r, "/pwpusher?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validator.ValidateIntRange(req.ExpiryDays, MinExpiryDays, MaxExpiryDays, "expiry days"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
w.Header().Set("Location", "/pwpusher?error="+url.QueryEscape(err.Error()))
|
||||
http.Redirect(w, r, "/pwpusher?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.validator.ValidateIntRange(req.MaxViews, MinViews, MaxViews, "max views"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
w.Header().Set("Location", "/pwpusher?error="+url.QueryEscape(err.Error()))
|
||||
http.Redirect(w, r, "/pwpusher?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -575,7 +569,6 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
||||
// Encrypt text with double encryption
|
||||
encryptedText, err := p.encryptWithKey(req.Text, additionalKey)
|
||||
if err != nil {
|
||||
log.Printf("Encryption error: %v", err)
|
||||
http.Error(w, "Failed to encrypt text", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -591,7 +584,6 @@ func (p *PWPusher) handleCreatePush(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Failed to hash password: %v", err)
|
||||
http.Error(w, "Failed to process password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -687,8 +679,6 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("ViewHandler: Extracted ID '%s' from URL '%s'", id, r.URL.Path)
|
||||
|
||||
// Handle POST requests (reveal actions and password verification)
|
||||
if r.Method == http.MethodPost {
|
||||
r.ParseForm()
|
||||
@@ -827,7 +817,6 @@ func (p *PWPusher) ViewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Password is correct - reset failed attempts and show content directly
|
||||
log.Printf("Correct password for IP %s, resetting attempts", clientIP)
|
||||
p.resetFailedAttempts(clientIP)
|
||||
|
||||
// Decrypt text with the encryption key
|
||||
@@ -1190,8 +1179,6 @@ func (p *PWPusher) setHistoryCookies(w http.ResponseWriter, history []map[string
|
||||
func (p *PWPusher) renderTemplate(w http.ResponseWriter, templateName string, data interface{}) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
log.Printf("Attempting to render template: %s", templateName)
|
||||
|
||||
// Auto-add CSRF token for ViewData structs
|
||||
if viewData, ok := data.(*ViewData); ok && viewData.CSRFToken == "" {
|
||||
if err := p.addCSRFToken(viewData); err != nil {
|
||||
@@ -1220,15 +1207,11 @@ func (p *PWPusher) renderTemplate(w http.ResponseWriter, templateName string, da
|
||||
|
||||
// Check if template exists
|
||||
if templates.Lookup(templateName) == nil {
|
||||
log.Printf("Template %s not found, using basic page", templateName)
|
||||
p.renderBasicPage(w, templateName, data)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Template %s found, executing with data: %+v", templateName, data)
|
||||
// Execute the specific template directly
|
||||
if err := templates.ExecuteTemplate(w, templateName, data); err != nil {
|
||||
log.Printf("Template error: %v", err)
|
||||
// Fallback to basic HTML
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><title>PWPusher</title></head><body>
|
||||
<h1>PWPusher - Template Error</h1>
|
||||
|
||||
263
web/base.html
263
web/base.html
@@ -1,108 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}HeaderAnalyzer{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<title>{{block "title" .}}{{end}}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
bg: '#1e1e1e',
|
||||
surface: '#2d2d2d',
|
||||
border: '#404040',
|
||||
text: '#e0e0e0',
|
||||
muted: '#999999'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Popup notification styles */
|
||||
.popup-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
max-width: 400px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
/* Custom animations and styles */
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.popup-notification.show {
|
||||
transform: translateX(0);
|
||||
@keyframes slideOutRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
.popup-notification.error {
|
||||
background: #f44336;
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.popup-notification.warning {
|
||||
background: #ff9800;
|
||||
.animate-slide-out-right {
|
||||
animation: slideOutRight 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
.popup-notification.success {
|
||||
background: #4caf50;
|
||||
/* Backdrop blur for popup */
|
||||
.backdrop-blur-popup {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.popup-notification.info {
|
||||
background: #2196f3;
|
||||
}
|
||||
|
||||
.popup-notification .close-btn {
|
||||
float: right;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.popup-notification .close-btn:hover {
|
||||
opacity: 0.7;
|
||||
/* Floating button styles */
|
||||
.floating-nav {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
{{block "head" .}}{{end}}
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" {{if eq .CurrentPage "home"}}class="active"{{end}}>Analyze New Header</a>
|
||||
<a href="/dns" {{if eq .CurrentPage "dns"}}class="active"{{end}}>DNS Tools</a>
|
||||
<a href="/password" {{if eq .CurrentPage "password"}}class="active"{{end}}>Password Generator</a>
|
||||
<a href="/pwpush" {{if eq .CurrentPage "pwpush"}}class="active"{{end}}>Password Pusher</a>
|
||||
</nav>
|
||||
<body class="bg-dark-bg text-dark-text min-h-screen">
|
||||
<!-- Floating Navigation -->
|
||||
<div class="fixed top-4 right-4 z-50 floating-nav">
|
||||
<div class="flex bg-dark-surface border border-dark-border rounded-full overflow-hidden">
|
||||
<!-- Home Button -->
|
||||
<a href="/" class="flex items-center justify-center w-12 h-12 bg-blue-600 hover:bg-blue-700 transition-colors duration-200 text-white">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<main>
|
||||
<!-- Menu Button -->
|
||||
<button id="menuButton" class="flex items-center justify-center w-12 h-12 bg-dark-surface hover:bg-gray-700 transition-colors duration-200 text-dark-text">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Popup -->
|
||||
<div id="navigationPopup" class="fixed inset-0 z-40 hidden">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-popup" onclick="closeNavigationPopup()"></div>
|
||||
|
||||
<!-- Popup Content -->
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-dark-surface border border-dark-border rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-dark-text">Navigation</h2>
|
||||
<button onclick="closeNavigationPopup()" class="text-dark-muted hover:text-dark-text transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IT Tools Category -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-blue-400 uppercase tracking-wide mb-3">IT Tools</h3>
|
||||
<div class="space-y-2">
|
||||
<a href="/analyze" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-green-600 rounded-lg mr-3 group-hover:bg-green-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-dark-text font-medium">Email Header Analyzer</div>
|
||||
<div class="text-dark-muted text-sm">Analyze email headers for security</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/dns" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-purple-600 rounded-lg mr-3 group-hover:bg-purple-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-dark-text font-medium">DNS Tools</div>
|
||||
<div class="text-dark-muted text-sm">DNS lookups and network diagnostics</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Online Security Category -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-orange-400 uppercase tracking-wide mb-3">Online Security</h3>
|
||||
<div class="space-y-2">
|
||||
<a href="/pwgenerator" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-yellow-600 rounded-lg mr-3 group-hover:bg-yellow-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-0.257-.257A6 6 0 1118 8zm-1.5 0a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM10 7a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-dark-text font-medium">Password Generator</div>
|
||||
<div class="text-dark-muted text-sm">Generate secure passwords</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/pwpush" class="flex items-center p-3 rounded-lg hover:bg-dark-bg transition-colors duration-200 group">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-red-600 rounded-lg mr-3 group-hover:bg-red-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-dark-text font-medium">Password Pusher</div>
|
||||
<div class="text-dark-muted text-sm">Share sensitive text securely</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-4 py-6 pt-8">
|
||||
{{block "content" .}}
|
||||
<div class="container">
|
||||
<h1>HeaderAnalyzer</h1>
|
||||
<p>Welcome to HeaderAnalyzer - your tool for email header analysis, DNS tools, and password generation.</p>
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold mb-4">HeaderAnalyzer</h1>
|
||||
<p class="text-dark-muted text-lg">Welcome to HeaderAnalyzer - your comprehensive toolkit for IT and security tools.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
<!-- Popup notification container -->
|
||||
<div id="popup-container"></div>
|
||||
<div id="popup-container" class="fixed top-4 left-4 z-50 space-y-2"></div>
|
||||
|
||||
<script>
|
||||
// Popup notification system
|
||||
// Navigation popup functionality
|
||||
function openNavigationPopup() {
|
||||
const popup = document.getElementById('navigationPopup');
|
||||
popup.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeNavigationPopup() {
|
||||
const popup = document.getElementById('navigationPopup');
|
||||
popup.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Menu button click handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const menuButton = document.getElementById('menuButton');
|
||||
menuButton.addEventListener('click', openNavigationPopup);
|
||||
});
|
||||
|
||||
// Popup notification system with Tailwind classes
|
||||
function showPopup(message, type = 'error', duration = 5000) {
|
||||
const container = document.getElementById('popup-container');
|
||||
const popup = document.createElement('div');
|
||||
popup.className = `popup-notification ${type}`;
|
||||
|
||||
let bgClass = 'bg-red-600';
|
||||
let iconSvg = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>`;
|
||||
|
||||
switch(type) {
|
||||
case 'success':
|
||||
bgClass = 'bg-green-600';
|
||||
iconSvg = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>`;
|
||||
break;
|
||||
case 'warning':
|
||||
bgClass = 'bg-yellow-600';
|
||||
iconSvg = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>`;
|
||||
break;
|
||||
case 'info':
|
||||
bgClass = 'bg-blue-600';
|
||||
iconSvg = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>`;
|
||||
break;
|
||||
}
|
||||
|
||||
popup.className = `${bgClass} text-white p-4 rounded-lg shadow-lg max-w-sm w-full animate-slide-in-right flex items-start space-x-3`;
|
||||
popup.innerHTML = `
|
||||
<button class="close-btn" onclick="this.parentElement.remove()">×</button>
|
||||
${message}
|
||||
<div class="flex-shrink-0">${iconSvg}</div>
|
||||
<div class="flex-grow">${message}</div>
|
||||
<button class="flex-shrink-0 text-white hover:text-gray-200 transition-colors" onclick="this.parentElement.remove()">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(popup);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => popup.classList.add('show'), 10);
|
||||
|
||||
// Auto-remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
popup.classList.remove('show');
|
||||
popup.classList.remove('animate-slide-in-right');
|
||||
popup.classList.add('animate-slide-out-right');
|
||||
setTimeout(() => popup.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
@@ -118,7 +250,6 @@
|
||||
|
||||
if (error) {
|
||||
showPopup(decodeURIComponent(error), 'error');
|
||||
// Clean URL
|
||||
urlParams.delete('error');
|
||||
window.history.replaceState({}, '', `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`);
|
||||
}
|
||||
|
||||
217
web/dns.html
217
web/dns.html
@@ -2,25 +2,21 @@
|
||||
|
||||
{{define "title"}}DNS Tools - HeaderAnalyzer{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
<style>
|
||||
.dns-tools-container { max-width: 900px; margin: 0 auto; }
|
||||
.dns-query-form { display: flex; gap: 10px; margin-bottom: 10px; }
|
||||
.dns-query-form input, .dns-query-form select { padding: 7px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0; }
|
||||
.dns-query-form button { padding: 7px 16px; }
|
||||
.dns-results { margin-top: 10px; }
|
||||
.dns-result-block { background: #232323; border-radius: 6px; margin-bottom: 12px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.12); }
|
||||
.dns-result-block pre { white-space: pre-wrap; word-break: break-word; font-size: 1em; }
|
||||
.save-btns { margin-bottom: 10px; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="dns-tools-container">
|
||||
<h1>DNS Tools</h1>
|
||||
<form class="dns-query-form" id="dnsForm" onsubmit="return false;">
|
||||
<input type="text" id="dnsInput" placeholder="Enter domain or IP" required>
|
||||
<select id="dnsType">
|
||||
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
||||
<div class="bg-gray-800 rounded-xl p-8 border border-gray-700 mb-8">
|
||||
<a href="/dns" 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">
|
||||
🌐 DNS Tools
|
||||
</h1>
|
||||
</a>
|
||||
<form class="flex flex-wrap gap-3 items-end mb-6" id="dnsForm" onsubmit="return false;">
|
||||
<div class="flex-1 min-w-64">
|
||||
<input type="text" id="dnsInput" placeholder="Enter domain or IP" required
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<select id="dnsType" class="px-4 py-3 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">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="MX">MX</option>
|
||||
@@ -34,27 +30,141 @@
|
||||
<option value="DMARC">DMARC</option>
|
||||
<option value="WHOIS">WHOIS</option>
|
||||
</select>
|
||||
<input type="text" id="dnsServer" placeholder="Custom DNS server (optional)" style="width:180px;" autocomplete="off">
|
||||
<button type="submit">Query</button>
|
||||
</form>
|
||||
<div class="save-btns">
|
||||
<button onclick="saveResults('csv')">Save as CSV</button>
|
||||
<button onclick="saveResults('txt')">Save as TXT</button>
|
||||
</div>
|
||||
<div class="dns-results" id="dnsResults"></div>
|
||||
<div class="min-w-48">
|
||||
<input type="text" id="dnsServer" placeholder="Custom DNS server (optional)"
|
||||
class="w-full px-4 py-3 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
|
||||
</div>
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-medium px-6 py-3 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<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>
|
||||
<span>Query</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button onclick="saveResults('csv')" class="bg-green-600 hover:bg-green-700 text-white font-medium px-4 py-2 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<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>
|
||||
<span>Save as CSV</span>
|
||||
</button>
|
||||
<button onclick="saveResults('txt')" class="bg-green-600 hover:bg-green-700 text-white font-medium px-4 py-2 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<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>
|
||||
<span>Save as TXT</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4" id="dnsResults"></div>
|
||||
</div>
|
||||
<style>
|
||||
.popup-notification {
|
||||
background-color: rgb(31 41 55);
|
||||
border: 1px solid rgb(75 85 99);
|
||||
color: rgb(243 244 246);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
max-width: 24rem;
|
||||
position: relative;
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.popup-notification.show {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.popup-notification.error {
|
||||
border-color: rgb(239 68 68);
|
||||
background-color: rgba(127 29 29, 0.5);
|
||||
}
|
||||
|
||||
.popup-notification.success {
|
||||
border-color: rgb(34 197 94);
|
||||
background-color: rgba(20 83 45, 0.5);
|
||||
}
|
||||
|
||||
.popup-notification.warning {
|
||||
border-color: rgb(234 179 8);
|
||||
background-color: rgba(133 77 14, 0.5);
|
||||
}
|
||||
|
||||
.popup-notification .close-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
color: rgb(156 163 175);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.popup-notification .close-btn:hover {
|
||||
color: rgb(229 231 235);
|
||||
}
|
||||
</style>
|
||||
<!-- 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) return;
|
||||
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) {
|
||||
@@ -69,11 +179,31 @@ document.getElementById('dnsForm').addEventListener('submit', function() {
|
||||
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 => response.text())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const result = {
|
||||
@@ -86,35 +216,45 @@ document.getElementById('dnsForm').addEventListener('submit', function() {
|
||||
results.push(result);
|
||||
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'dns-result-block';
|
||||
resultDiv.className = 'bg-gray-800 border border-gray-700 rounded-lg p-6 mb-4';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>${type} query for ${query}</h3>
|
||||
<p><small>Time: ${timestamp} | Server: ${server || 'Default'}</small></p>
|
||||
<pre>${data}</pre>
|
||||
<h3 class="text-blue-400 font-semibold mb-2">${type} query for ${query}</h3>
|
||||
<p class="text-gray-400 text-sm mb-3">Time: ${timestamp} | Server: ${server || 'Default'}</p>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg overflow-x-auto text-sm border border-gray-700">${data}</pre>
|
||||
`;
|
||||
document.getElementById('dnsResults').insertBefore(resultDiv, document.getElementById('dnsResults').firstChild);
|
||||
|
||||
showPopup(`Successfully queried ${type} record for ${query}`, 'success', 3000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
console.error('DNS Query Error:', error);
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'dns-result-block';
|
||||
resultDiv.className = 'bg-gray-800 border border-red-500/50 rounded-lg p-6 mb-4';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>Error querying ${query}</h3>
|
||||
<pre>Error: ${error.message}</pre>
|
||||
<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 border border-gray-700">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) {
|
||||
alert('No results to save');
|
||||
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 => {
|
||||
@@ -139,6 +279,11 @@ function saveResults(format) {
|
||||
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}}
|
||||
|
||||
224
web/dns.html.old
Normal file
224
web/dns.html.old
Normal file
@@ -0,0 +1,224 @@
|
||||
{{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}}
|
||||
@@ -9,312 +9,547 @@
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<h1>Email Header Analyzer</h1>
|
||||
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
||||
<div class="text-center mb-8">
|
||||
<a href="/analyze" 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">
|
||||
📧 Email Header Analyzer
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{if not .From}}
|
||||
<form method="POST">
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 max-w-4xl mx-auto">
|
||||
<form method="POST" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<textarea name="headers" placeholder="Paste email headers here..." required></textarea>
|
||||
<br>
|
||||
<button type="submit">Analyze Headers</button>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-200 mb-2">📝 Email Headers:</label>
|
||||
<textarea name="headers"
|
||||
placeholder="Paste email headers here..."
|
||||
required
|
||||
rows="12"
|
||||
class="w-full p-4 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 font-mono text-sm placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none resize-y"></textarea>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-lg transition-colors">
|
||||
🔍 Analyze Headers
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .From}}
|
||||
<div id="report" class="container">
|
||||
<div class="section" style="display: flex; align-items: flex-start; justify-content: space-between; gap: 30px;">
|
||||
<div style="flex: 1 1 0; min-width: 0;">
|
||||
<h2>Sender Identification</h2>
|
||||
<div class="grid">
|
||||
<div id="report" class="space-y-6">
|
||||
<!-- Sender Identification -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">👤 Sender Identification</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p><b>Envelope Sender (Return-Path):</b> {{.EnvelopeSender}}</p>
|
||||
<p><b>From Domain:</b> {{.FromDomain}}</p>
|
||||
<p><b>Sending Server:</b> {{.SendingServer}}</p>
|
||||
<span class="text-sm font-medium text-gray-400">Envelope Sender (Return-Path):</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.EnvelopeSender}}</p>
|
||||
</div>
|
||||
<div class="score-indicators">
|
||||
<span class="status {{if .SPFPass}}good{{else}}error{{end}}" title="SPF">SPF {{if .SPFPass}}✓{{else}}✗{{end}}</span>
|
||||
<span class="status {{if .DMARCPass}}good{{else}}error{{end}}" title="DMARC">DMARC {{if .DMARCPass}}✓{{else}}✗{{end}}</span>
|
||||
<span class="status {{if .DKIMPass}}good{{else}}error{{end}}" title="DKIM">DKIM {{if .DKIMPass}}✓{{else}}✗{{end}}</span>
|
||||
<span class="status {{if .Encrypted}}good{{else}}error{{end}}" title="Encrypted">Encrypted {{if .Encrypted}}✓{{else}}✗{{end}}</span>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">From Domain:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.FromDomain}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Sending Server:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.SendingServer}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">🔒 Security Status</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .SPFPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
SPF {{if .SPFPass}}✓{{else}}✗{{end}}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .DMARCPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
DMARC {{if .DMARCPass}}✓{{else}}✗{{end}}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .DKIMPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
DKIM {{if .DKIMPass}}✓{{else}}✗{{end}}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .Encrypted}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
Encrypted {{if .Encrypted}}✓{{else}}✗{{end}}
|
||||
</span>
|
||||
{{if .Blacklists}}
|
||||
<span class="status error" title="{{range .Blacklists}}{{.}}, {{end}}">Blacklisted {{len .Blacklists}} times</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-900 text-red-200 border border-red-600">
|
||||
Blacklisted {{len .Blacklists}} times
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="status good">Not listed on major blacklists</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-900 text-green-200 border border-green-600">
|
||||
Not Blacklisted
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .SenderRep}}
|
||||
<div>
|
||||
<b><span>Sender Reputation: </span></b><div class="status {{if contains .SenderRep "EXCELLENT"}}good{{else if contains .SenderRep "GOOD"}}good{{else if contains .SenderRep "FAIR"}}warning{{else}}error{{end}}">
|
||||
<div class="mt-4">
|
||||
<span class="text-sm font-medium text-gray-400">Sender Reputation:</span>
|
||||
<div class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium mt-1 {{if contains .SenderRep "EXCELLENT"}}bg-green-900 text-green-200 border border-green-600{{else if contains .SenderRep "GOOD"}}bg-green-900 text-green-200 border border-green-600{{else if contains .SenderRep "FAIR"}}bg-yellow-900 text-yellow-200 border border-yellow-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
{{.SenderRep}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="explanation">
|
||||
<small>
|
||||
<b>Envelope Sender</b> is the real sender used for delivery (can differ from From).<br>
|
||||
<b>From Domain</b> is the domain shown to the recipient.<br>
|
||||
<b>Sending Server</b> is the host or IP that actually sent the message (from first Received header).<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 p-4 bg-gray-900 rounded-lg border border-gray-600">
|
||||
<p class="text-sm text-gray-300">
|
||||
<strong class="text-gray-200">Note:</strong> The <strong>Envelope Sender</strong> is the real sender used for delivery (can differ from From).
|
||||
The <strong>From Domain</strong> is shown to the recipient. The <strong>Sending Server</strong> is the host/IP that sent the message.
|
||||
If these differ, the message may be sent on behalf of another user or via a third-party service.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- All Headers Table -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700">
|
||||
<details class="p-6">
|
||||
<summary class="text-xl font-bold text-gray-100 cursor-pointer hover:text-blue-400 transition-colors">
|
||||
📋 All Email Headers Table
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<input type="text"
|
||||
id="headerSearch"
|
||||
placeholder="Search headers..."
|
||||
class="w-full max-w-md px-4 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none">
|
||||
</div>
|
||||
<details id="all-headers" class="section" style="margin-top:10px;">
|
||||
<summary><b style="font-size: 1.5em;">All Email Headers Table</b></summary>
|
||||
<div style="margin-bottom:10px;">
|
||||
<input type="text" id="headerSearch" placeholder="Search headers..." style="width: 100%; max-width: 350px; padding: 5px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0;">
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table id="headersTable" style="width:100%; border-collapse:collapse; border:1px solid #444;">
|
||||
<thead>
|
||||
<div class="overflow-x-auto">
|
||||
<table id="headersTable" class="w-full border border-gray-600 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-900">
|
||||
<tr>
|
||||
<th style="text-align:left; padding:4px 8px; border:1px solid #444; width: 180px; background:#232323;">Header Name</th>
|
||||
<th style="text-align:left; padding:4px 8px; border:1px solid #444; background:#232323;">Value</th>
|
||||
<th class="text-left px-4 py-3 text-gray-200 font-medium border-b border-gray-600 w-48">Header Name</th>
|
||||
<th class="text-left px-4 py-3 text-gray-200 font-medium border-b border-gray-600">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="divide-y divide-gray-600">
|
||||
{{range $k, $v := .AllHeaders}}
|
||||
<tr>
|
||||
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; word-break:break-word;">{{$k}}</td>
|
||||
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; white-space:pre-wrap; word-break:break-word;">{{$v}}</td>
|
||||
<tr class="hover:bg-gray-700/50">
|
||||
<td class="px-4 py-3 text-gray-300 font-mono text-sm break-words border-r border-gray-600">{{$k}}</td>
|
||||
<td class="px-4 py-3 text-gray-100 font-mono text-sm whitespace-pre-wrap break-words">{{$v}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Basic Information</h2>
|
||||
<div class="grid">
|
||||
{{if .PhishingRisk}}
|
||||
<!-- Security Risk Assessment -->
|
||||
<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 Risk Assessment</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">Phishing Risk</h3>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if eq (index (splitString .PhishingRisk " ") 0) "HIGH"}}bg-red-900 text-red-200 border border-red-600{{else if eq (index (splitString .PhishingRisk " ") 0) "MEDIUM"}}bg-yellow-900 text-yellow-200 border border-yellow-600{{else}}bg-green-900 text-green-200 border border-green-600{{end}}">
|
||||
{{.PhishingRisk}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">Spoofing Risk</h3>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if contains .SpoofingRisk "POTENTIAL"}}bg-yellow-900 text-yellow-200 border border-yellow-600{{else}}bg-green-900 text-green-200 border border-green-600{{end}}">
|
||||
{{.SpoofingRisk}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">📧 Basic Information</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p><b>From:</b> {{.From}}</p>
|
||||
<p><b>To:</b> {{.To}}</p>
|
||||
<p><b>Subject:</b> {{.Subject}}</p>
|
||||
<p><b>Date:</b> {{.Date}}</p>
|
||||
{{if .ReplyTo}}<p><b>Reply-To:</b> {{.ReplyTo}}</p>{{end}}
|
||||
<span class="text-sm font-medium text-gray-400">From:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded break-all">{{.From}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><b>Message-ID:</b> {{.MessageID}}</p>
|
||||
<p><b>Priority:</b> {{.Priority}}</p>
|
||||
<p><b>Content Type:</b> {{.ContentType}}</p>
|
||||
<p><b>Encoding:</b> {{.Encoding}}</p>
|
||||
<span class="text-sm font-medium text-gray-400">To:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded break-all">{{.To}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Subject:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded break-words">{{.Subject}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Date:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.Date}}</p>
|
||||
</div>
|
||||
{{if .ReplyTo}}
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Reply-To:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded break-all">{{.ReplyTo}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Message-ID:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded break-all">{{.MessageID}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Priority:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.Priority}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Content Type:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.ContentType}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Encoding:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.Encoding}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Mail Flow</h2>
|
||||
<ul class="mail-flow">
|
||||
{{range .Received}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- Mail Flow -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔄 Mail Flow</h2>
|
||||
<div class="space-y-3">
|
||||
{{range $index, $received := .Received}}
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||
{{add $index 1}}
|
||||
</div>
|
||||
<div class="flex-1 p-3 bg-gray-900 rounded-lg border border-gray-600">
|
||||
<p class="text-gray-100 font-mono text-sm">{{$received}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery Analysis -->
|
||||
{{if ne .DeliveryDelay "Insufficient data for delay analysis"}}
|
||||
<div class="section">
|
||||
<h2>Delivery Analysis</h2>
|
||||
<p><b>Delivery Timing:</b> {{.DeliveryDelay}}</p>
|
||||
{{if .GeoLocation}}<p><b>Geographic Info:</b> {{.GeoLocation}}</p>{{end}}
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">⏱️ Delivery Analysis</h2>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Delivery Timing:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.DeliveryDelay}}</p>
|
||||
</div>
|
||||
{{if .GeoLocation}}
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-400">Geographic Info:</span>
|
||||
<p class="text-gray-100 font-mono text-sm bg-gray-900 p-2 rounded">{{.GeoLocation}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="section">
|
||||
<h2>Security Analysis</h2>
|
||||
<div class="security-analysis-vertical">
|
||||
<div class="section">
|
||||
<h3>SPF Authentication</h3>
|
||||
<div class="status {{if .SPFPass}}good{{else}}error{{end}}">
|
||||
<!-- Security Analysis -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔒 Security Analysis</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- SPF Authentication -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">SPF Authentication</h3>
|
||||
<div class="mb-3">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .SPFPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
{{if .SPFPass}}✓ Passed{{else}}✗ Failed{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<p>{{.SPFDetails}}</p>
|
||||
{{if .SPFRecord}}<pre>{{.SPFRecord}}</pre>{{end}}
|
||||
<p class="text-gray-300 text-sm mb-3">{{.SPFDetails}}</p>
|
||||
{{if .SPFRecord}}
|
||||
<div class="mb-3">
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600">{{.SPFRecord}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .SPFHeader}}
|
||||
<details class="details"><summary>Show SPF Header</summary><pre>{{.SPFHeader}}</pre></details>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300">Show SPF Header</summary>
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600 mt-2">{{.SPFHeader}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>DMARC Policy</h3>
|
||||
<div class="status {{if .DMARCPass}}good{{else}}error{{end}}">
|
||||
<!-- DMARC Policy -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">DMARC Policy</h3>
|
||||
<div class="mb-3">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .DMARCPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
{{if .DMARCPass}}✓ Passed{{else}}✗ Failed{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<p>{{.DMARCDetails}}</p>
|
||||
{{if .DMARCRecord}}<pre>{{.DMARCRecord}}</pre>{{end}}
|
||||
<p class="text-gray-300 text-sm mb-3">{{.DMARCDetails}}</p>
|
||||
{{if .DMARCRecord}}
|
||||
<div class="mb-3">
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600">{{.DMARCRecord}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .DMARCHeader}}
|
||||
<details class="details"><summary>Show DMARC Header</summary><pre>{{.DMARCHeader}}</pre></details>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300">Show DMARC Header</summary>
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600 mt-2">{{.DMARCHeader}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>DKIM Signature</h3>
|
||||
<div class="status {{if .DKIMPass}}good{{else}}error{{end}}">
|
||||
<!-- DKIM Signature -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-gray-200 mb-3">DKIM Signature</h3>
|
||||
<div class="mb-3">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{if .DKIMPass}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
{{if .DKIMPass}}✓ Present{{else}}✗ Missing{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<p>{{.DKIMDetails}}</p>
|
||||
<p class="text-gray-300 text-sm mb-3">{{.DKIMDetails}}</p>
|
||||
{{if .DKIM}}
|
||||
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIM}}</pre></details>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300">Show DKIM Header</summary>
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600 mt-2">{{.DKIM}}</pre>
|
||||
</details>
|
||||
{{else if .DKIMHeader}}
|
||||
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIMHeader}}</pre></details>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300">Show DKIM Header</summary>
|
||||
<pre class="bg-gray-800 text-gray-100 p-3 rounded text-xs overflow-x-auto border border-gray-600 mt-2">{{.DKIMHeader}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Encryption</h2>
|
||||
<div class="status {{if .Encrypted}}good{{else}}error{{end}}">
|
||||
{{if .Encrypted}}Encrypted (TLS){{else}}Not Encrypted{{end}}
|
||||
<!-- Encryption -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔐 Encryption</h2>
|
||||
<div class="mb-4">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium {{if .Encrypted}}bg-green-900 text-green-200 border border-green-600{{else}}bg-red-900 text-red-200 border border-red-600{{end}}">
|
||||
{{if .Encrypted}}🔒 Encrypted (TLS){{else}}🔓 Not Encrypted{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<details><summary>Show Encryption Details</summary><pre>{{.EncryptionDetail}}</pre></details>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300 mb-2">Show Encryption Details</summary>
|
||||
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto border border-gray-600">{{.EncryptionDetail}}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{{if .Warnings}}
|
||||
<div class="section">
|
||||
<h2>Warnings</h2>
|
||||
<ul>
|
||||
{{range .Warnings}}<li class="status warning">⚠️ {{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- Warnings -->
|
||||
<div class="bg-yellow-900/20 rounded-lg p-6 border border-yellow-600">
|
||||
<h2 class="text-2xl font-bold text-yellow-200 mb-4">⚠️ Warnings</h2>
|
||||
<div class="space-y-2">
|
||||
{{range .Warnings}}
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-yellow-400 mt-1">⚠️</span>
|
||||
<p class="text-yellow-100">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .SecurityFlags}}
|
||||
<div class="section">
|
||||
<h2>Security Flags</h2>
|
||||
<ul>
|
||||
{{range .SecurityFlags}}<li class="status">🔒 {{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- Security Flags -->
|
||||
<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 Flags</h2>
|
||||
<div class="space-y-2">
|
||||
{{range .SecurityFlags}}
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-green-400 mt-1">🔒</span>
|
||||
<p class="text-gray-100">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Blacklists}}
|
||||
<div class="section">
|
||||
<h2>Blacklist Status</h2>
|
||||
<div style="margin-bottom: 6px;">
|
||||
<b>Checked:</b>
|
||||
<!-- Blacklist Status -->
|
||||
<div class="bg-red-900/20 rounded-lg p-6 border border-red-600">
|
||||
<h2 class="text-2xl font-bold text-red-200 mb-4">🚫 Blacklist Status</h2>
|
||||
<div class="mb-4">
|
||||
<span class="text-sm font-medium text-gray-400">Checked:</span>
|
||||
<span class="text-gray-100 ml-2">
|
||||
{{if .SendingServer}}IP {{.SendingServer}}{{else if .FromDomain}}Domain {{.FromDomain}}{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium bg-red-900 text-red-200 border border-red-600">
|
||||
⚠️ Listed on the following blacklists
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{{range .Blacklists}}
|
||||
<div class="bg-red-900/30 p-3 rounded border border-red-600">
|
||||
<p class="text-red-100 text-sm">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="status error">⚠️ Listed on the following blacklists:</div>
|
||||
<ul>
|
||||
{{range .Blacklists}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{if .SpamFlags}}
|
||||
<div class="section">
|
||||
<h2>Spam Analysis</h2>
|
||||
<div class="status {{if gt (len .SpamFlags) 0}}warning{{else}}good{{end}}">
|
||||
<!-- Spam Analysis -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🛡️ Spam Analysis</h2>
|
||||
<div class="mb-4">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium {{if gt (len .SpamFlags) 0}}bg-yellow-900 text-yellow-200 border border-yellow-600{{else}}bg-green-900 text-green-200 border border-green-600{{end}}">
|
||||
{{if gt (len .SpamFlags) 0}}⚠️ Spam Indicators Found{{else}}✓ No Spam Indicators{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{if .SpamScore}}<p><b>Spam Score:</b> {{.SpamScore}}</p>{{end}}
|
||||
{{if .SpamScore}}
|
||||
<div class="mb-4">
|
||||
<span class="text-sm font-medium text-gray-400">Spam Score:</span>
|
||||
<span class="text-gray-100 ml-2 font-mono">{{.SpamScore}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .SpamFlags}}
|
||||
<ul>
|
||||
{{range .SpamFlags}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
<div class="space-y-2">
|
||||
{{range .SpamFlags}}
|
||||
<div class="bg-yellow-900/20 p-3 rounded border border-yellow-600">
|
||||
<p class="text-yellow-100 text-sm">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if ne .VirusInfo "No virus scanning information found"}}
|
||||
<div class="section">
|
||||
<h2>Virus Scanning</h2>
|
||||
<div class="status good">🛡️ Virus Scanning Information</div>
|
||||
<p>{{.VirusInfo}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .PhishingRisk}}
|
||||
<div class="section">
|
||||
<h2>Security Risk Assessment</h2>
|
||||
<div class="security-analysis-vertical">
|
||||
<div class="section">
|
||||
<h3>Phishing Risk</h3>
|
||||
<div class="status {{if eq (index (splitString .PhishingRisk " ") 0) "HIGH"}}error{{else if eq (index (splitString .PhishingRisk " ") 0) "MEDIUM"}}warning{{else}}good{{end}}">
|
||||
{{.PhishingRisk}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>Spoofing Risk</h3>
|
||||
<div class="status {{if contains .SpoofingRisk "POTENTIAL"}}warning{{else}}good{{end}}">
|
||||
{{.SpoofingRisk}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Virus Scanning -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🛡️ Virus Scanning</h2>
|
||||
<div class="mb-4">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium bg-green-900 text-green-200 border border-green-600">
|
||||
🛡️ Virus Scanning Information
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-100">{{.VirusInfo}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ListInfo}}
|
||||
<div class="section">
|
||||
<h2>Mailing List Information</h2>
|
||||
<ul>
|
||||
{{range .ListInfo}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
{{if .AutoReply}}<p class="status">📧 Auto-reply message detected</p>{{end}}
|
||||
{{if .BulkEmail}}<p class="status">📬 Bulk/marketing email detected</p>{{end}}
|
||||
<!-- Mailing List Information -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">📬 Mailing List Information</h2>
|
||||
<div class="space-y-2 mb-4">
|
||||
{{range .ListInfo}}
|
||||
<div class="bg-gray-900 p-3 rounded border border-gray-600">
|
||||
<p class="text-gray-100 text-sm">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{if .AutoReply}}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-900 text-blue-200 border border-blue-600">
|
||||
📧 Auto-reply message detected
|
||||
</span>
|
||||
{{end}}
|
||||
{{if .BulkEmail}}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-900 text-blue-200 border border-blue-600">
|
||||
📬 Bulk/marketing email detected
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Compliance}}
|
||||
<div class="section">
|
||||
<h2>Compliance Information</h2>
|
||||
<ul>
|
||||
{{range .Compliance}}<li class="status good">✓ {{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- Compliance Information -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">✅ Compliance Information</h2>
|
||||
<div class="space-y-2">
|
||||
{{range .Compliance}}
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-green-400 mt-1">✓</span>
|
||||
<p class="text-gray-100">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ARC}}
|
||||
<div class="section">
|
||||
<h2>ARC (Authenticated Received Chain)</h2>
|
||||
<details><summary>Show ARC Headers</summary>
|
||||
<ul>
|
||||
{{range .ARC}}<li><pre>{{.}}</pre></li>{{end}}
|
||||
</ul>
|
||||
<!-- ARC (Authenticated Received Chain) -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔗 ARC (Authenticated Received Chain)</h2>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300 mb-2">Show ARC Headers</summary>
|
||||
<div class="space-y-3 mt-4">
|
||||
{{range .ARC}}
|
||||
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto border border-gray-600">{{.}}</pre>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if ne .BIMI "No BIMI record found"}}
|
||||
<div class="section">
|
||||
<h2>Brand Indicators (BIMI)</h2>
|
||||
<p>{{.BIMI}}</p>
|
||||
<!-- Brand Indicators (BIMI) -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🎨 Brand Indicators (BIMI)</h2>
|
||||
<p class="text-gray-100">{{.BIMI}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Attachments}}
|
||||
<div class="section">
|
||||
<h2>Attachment Information</h2>
|
||||
<ul>
|
||||
{{range .Attachments}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- Attachment Information -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">📎 Attachment Information</h2>
|
||||
<div class="space-y-2">
|
||||
{{range .Attachments}}
|
||||
<div class="bg-gray-900 p-3 rounded border border-gray-600">
|
||||
<p class="text-gray-100 text-sm font-mono">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .URLs}}
|
||||
<div class="section">
|
||||
<h2>URL Information</h2>
|
||||
<ul>
|
||||
{{range .URLs}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
<!-- URL Information -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🔗 URL Information</h2>
|
||||
<div class="space-y-2">
|
||||
{{range .URLs}}
|
||||
<div class="bg-gray-900 p-3 rounded border border-gray-600">
|
||||
<p class="text-gray-100 text-sm font-mono break-all">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if ne .ThreadInfo "No threading information available"}}
|
||||
<div class="section">
|
||||
<h2>Message Threading</h2>
|
||||
<details><summary>Show Threading Information</summary>
|
||||
<pre>{{.ThreadInfo}}</pre>
|
||||
<!-- Message Threading -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-2xl font-bold text-gray-100 mb-4">🧵 Message Threading</h2>
|
||||
<details class="text-sm">
|
||||
<summary class="text-blue-400 cursor-pointer hover:text-blue-300 mb-2">Show Threading Information</summary>
|
||||
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto border border-gray-600 mt-4">{{.ThreadInfo}}</pre>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
<div class="section">
|
||||
<button onclick="exportPDF()" type="button">Export as PDF</button>
|
||||
<button onclick="exportImage()" type="button">Save as Image</button>
|
||||
<button onclick="printReport()" type="button">Print Report</button>
|
||||
<!-- Export Options -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 text-center">
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-4">📄 Export Options</h2>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<button onclick="exportPDF()"
|
||||
type="button"
|
||||
class="bg-red-600 hover:bg-red-700 text-white font-medium px-6 py-3 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<span>📄</span>
|
||||
<span>Export as PDF</span>
|
||||
</button>
|
||||
<button onclick="exportImage()"
|
||||
type="button"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white font-medium px-6 py-3 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<span>🖼️</span>
|
||||
<span>Save as Image</span>
|
||||
</button>
|
||||
<button onclick="printReport()"
|
||||
type="button"
|
||||
class="bg-green-600 hover:bg-green-700 text-white font-medium px-6 py-3 rounded-lg transition-colors flex items-center space-x-2">
|
||||
<span>🖨️</span>
|
||||
<span>Print Report</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -355,6 +590,10 @@
|
||||
return str.includes(substr);
|
||||
}
|
||||
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
function exportImage() {
|
||||
if (typeof html2canvas === 'undefined') {
|
||||
alert('Image export library not loaded. Please refresh the page and try again.');
|
||||
|
||||
158
web/landing_page.html
Normal file
158
web/landing_page.html
Normal file
@@ -0,0 +1,158 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}HeaderAnalyzer - IT & Security Tools{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
<style>
|
||||
/* Additional custom styles for landing page */
|
||||
.feature-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
background: linear-gradient(45deg, #3b82f6, #8b5cf6);
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="min-h-screen">
|
||||
<!-- Hero Section -->
|
||||
<div class="text-center mb-16">
|
||||
<div class="gradient-bg rounded-3xl p-12 mb-8 shadow-2xl">
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-white" style="border-bottom: none;">IT & Security Tools</h1>
|
||||
<!--
|
||||
<p class="text-xl md:text-2xl text-blue-100 mb-8 max-w-3xl mx-auto">
|
||||
Your comprehensive toolkit for IT diagnostics and online security
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4 text-sm text-blue-200">
|
||||
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Email Security</span>
|
||||
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Password Security</span>
|
||||
<span class="bg-white bg-opacity-20 px-4 py-2 rounded-full">Data Protection</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools Categories -->
|
||||
<div class="max-w-6xl mx-auto space-y-12">
|
||||
|
||||
<!-- IT Tools Category -->
|
||||
<div class="category-section">
|
||||
<div class="flex items-center mb-8">
|
||||
<div class="category-badge text-white px-4 py-2 rounded-full text-sm font-semibold mr-4">
|
||||
IT TOOLS
|
||||
</div>
|
||||
<div class="flex-1 h-px bg-dark-border"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<!-- Email Header Analyzer -->
|
||||
<a href="/analyze" class="feature-card block group">
|
||||
<div class="bg-dark-surface border border-dark-border rounded-xl p-6 h-full hover:border-green-500 transition-all duration-300">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-center justify-center w-12 h-12 bg-green-600 rounded-xl group-hover:bg-green-700 transition-colors">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-dark-text mb-2 group-hover:text-green-400 transition-colors">
|
||||
Email Header Analyzer
|
||||
</h3>
|
||||
<p class="text-dark-muted mb-4">
|
||||
Analyze email headers to detect security threats, trace origins, and validate email authenticity. Essential for cybersecurity professionals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- DNS Tools -->
|
||||
<a href="/dns" class="feature-card block group">
|
||||
<div class="bg-dark-surface border border-dark-border rounded-xl p-6 h-full hover:border-purple-500 transition-all duration-300">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-center justify-center w-12 h-12 bg-purple-600 rounded-xl group-hover:bg-purple-700 transition-colors">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-dark-text mb-2 group-hover:text-purple-400 transition-colors">
|
||||
DNS Tools
|
||||
</h3>
|
||||
<p class="text-dark-muted mb-4">
|
||||
Perform comprehensive DNS lookups, diagnose network issues, and verify domain configurations. Perfect for system administrators.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Online Security Category -->
|
||||
<div class="category-section">
|
||||
<div class="flex items-center mb-8">
|
||||
<div class="bg-gradient-to-r from-orange-500 to-red-500 text-white px-4 py-2 rounded-full text-sm font-semibold mr-4">
|
||||
ONLINE SECURITY
|
||||
</div>
|
||||
<div class="flex-1 h-px bg-dark-border"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<!-- Password Generator -->
|
||||
<a href="/pwgenerator" class="feature-card block group">
|
||||
<div class="bg-dark-surface border border-dark-border rounded-xl p-6 h-full hover:border-yellow-500 transition-all duration-300">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-center justify-center w-12 h-12 bg-yellow-600 rounded-xl group-hover:bg-yellow-700 transition-colors">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-0.257-.257A6 6 0 1118 8zm-1.5 0a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM10 7a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-dark-text mb-2 group-hover:text-yellow-400 transition-colors">
|
||||
Password Generator
|
||||
</h3>
|
||||
<p class="text-dark-muted mb-4">
|
||||
Generate cryptographically secure passwords with customizable options. Enhance your digital security with strong, unique passwords.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Password Pusher -->
|
||||
<a href="/pwpush" class="feature-card block group">
|
||||
<div class="bg-dark-surface border border-dark-border rounded-xl p-6 h-full hover:border-red-500 transition-all duration-300">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-center justify-center w-12 h-12 bg-red-600 rounded-xl group-hover:bg-red-700 transition-colors">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-dark-text mb-2 group-hover:text-red-400 transition-colors">
|
||||
Password Pusher
|
||||
</h3>
|
||||
<p class="text-dark-muted mb-4">
|
||||
Securely share sensitive information with automatic expiration and view limits. Perfect for sharing passwords and confidential data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -3,123 +3,195 @@
|
||||
{{define "title"}}Password Generator - HeaderAnalyzer{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="password-generator">
|
||||
<h1>🔐 Password Generator</h1>
|
||||
|
||||
<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}}">
|
||||
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-btn" id="randomTab">Random Password</button>
|
||||
<button class="tab-btn active" id="passphraseTab">Passphrase</button>
|
||||
<!-- 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>
|
||||
|
||||
<div class="password-output">
|
||||
<div class="password-text" id="passwordDisplay">Click "Generate Password" to create a secure password</div>
|
||||
<button class="copy-btn" id="copyBtn" onclick="copyPassword()" style="display: none;">Copy to Clipboard</button>
|
||||
</div>
|
||||
|
||||
<button class="generate-btn" onclick="generatePassword()">🎲 Generate Password</button>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>🔧 Basic Settings</h3>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="length">Password Length:</label>
|
||||
<input type="number" id="length" min="4" max="128" value="{{.Config.Length}}" onchange="updateURL()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="includeUpper">Include Uppercase (A-Z):</label>
|
||||
<input type="checkbox" id="includeUpper" {{if .Config.IncludeUpper}}checked{{end}} onchange="updateURL()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="includeLower">Include Lowercase (a-z):</label>
|
||||
<input type="checkbox" id="includeLower" {{if .Config.IncludeLower}}checked{{end}} onchange="updateURL()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="numberCount">Number of Digits:</label>
|
||||
<input type="number" id="numberCount" min="0" max="20" value="{{.Config.NumberCount}}" onchange="updateURL()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="specialChars">Special Characters:</label>
|
||||
<input type="text" id="specialChars" value="{{.Config.SpecialChars}}" onchange="updateURL()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="noConsecutive">No consecutive identical characters:</label>
|
||||
<input type="checkbox" id="noConsecutive" {{if .Config.NoConsecutive}}checked{{end}} onchange="updateURL()">
|
||||
<!-- 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="control-group">
|
||||
<h3>🎯 Advanced Settings</h3>
|
||||
|
||||
<div class="passphrase-controls {{if eq .Config.Type "passphrase"}}active{{end}}" id="passphraseControls">
|
||||
<div class="form-row">
|
||||
<label for="wordCount">Number of Words:</label>
|
||||
<input type="number" id="wordCount" min="2" max="10" value="{{.Config.WordCount}}" onchange="updateURL()">
|
||||
<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>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="passphraseUseNumbers">Include Numbers:</label>
|
||||
<input type="checkbox" id="passphraseUseNumbers" {{if .Config.UseNumbers}}checked{{end}} onchange="updateURL()">
|
||||
<!-- 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>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="passphraseUseSpecial">Include Special Characters:</label>
|
||||
<input type="checkbox" id="passphraseUseSpecial" {{if .Config.UseSpecial}}checked{{end}} onchange="updateURL()">
|
||||
<!-- 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 class="form-row">
|
||||
<label for="numberPosition">Number Position:</label>
|
||||
<select id="numberPosition" onchange="updateURL()">
|
||||
<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 class="form-row">
|
||||
<label>Strength Indicator:</label>
|
||||
<div id="strengthIndicator" style="color: #999;">Generate a password to see strength</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Word List Status:</label>
|
||||
<div id="wordListStatus" style="color: #999; font-size: 12px;">Loading...</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>
|
||||
|
||||
<!-- Settings Management -->
|
||||
<div class="control-group" style="margin-top: 20px;">
|
||||
<h3>💾 Settings & History</h3>
|
||||
<div class="form-row">
|
||||
<button onclick="saveSettings()" style="background: #007acc; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-right: 10px;">Save Current Settings</button>
|
||||
<button onclick="loadSettingsManual()" style="background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-right: 10px;">Load Saved Settings</button>
|
||||
<button onclick="clearSettings()" style="background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Clear Saved Settings</button>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="savePasswords">Save Generated Passwords (in web browser cookies only):</label>
|
||||
<input type="checkbox" id="savePasswords" {{if .Config.SavePasswords}}checked{{end}} onchange="togglePasswordSaving(); updateURL()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password History -->
|
||||
<div class="control-group" style="margin-top: 20px;">
|
||||
<h3>📚 Password History</h3>
|
||||
<div id="passwordHistory" style="max-height: 200px; overflow-y: auto; background: #1a1a1a; border: 1px solid #333; border-radius: 4px; padding: 10px;">
|
||||
<p style="color: #999; font-style: italic;">No passwords generated yet</p>
|
||||
<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 class="form-row" style="margin-top: 10px;">
|
||||
<button onclick="clearHistory()" style="background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Clear History</button>
|
||||
|
||||
<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>
|
||||
@@ -142,63 +214,101 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load password history
|
||||
loadPasswordHistory();
|
||||
|
||||
// Load word list info
|
||||
loadWordListInfo();
|
||||
|
||||
// Auto-generate if URL has parameters (excluding default)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.toString()) {
|
||||
generatePassword();
|
||||
}
|
||||
|
||||
// Add event listeners to automatically save settings on change
|
||||
document.querySelectorAll('input, select').forEach(element => {
|
||||
element.addEventListener('change', function() {
|
||||
updateURL();
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
// Note: Removed auto-save event listeners to prevent excessive saving notifications
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
function switchTab(mode) {
|
||||
currentMode = mode;
|
||||
|
||||
document.getElementById('randomTab').classList.toggle('active', mode === 'random');
|
||||
document.getElementById('passphraseTab').classList.toggle('active', mode === 'passphrase');
|
||||
document.getElementById('passphraseControls').classList.toggle('active', mode === 'passphrase');
|
||||
// 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();
|
||||
saveSettings();
|
||||
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
|
||||
// Define default values (savePasswords excluded from URL parameters)
|
||||
const defaults = {
|
||||
type: "passphrase",
|
||||
length: 12,
|
||||
includeUpper: true,
|
||||
includeLower: true,
|
||||
numberCount: 1,
|
||||
specialChars: "!@#$%^&*-_=+",
|
||||
specialChars: "!@#$%&*-_=+.",
|
||||
noConsecutive: false,
|
||||
wordCount: 3,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
numberPosition: "end",
|
||||
savePasswords: false
|
||||
numberPosition: "end"
|
||||
};
|
||||
|
||||
// Only add parameters that differ from defaults
|
||||
// Only add parameters that differ from defaults (excluding savePasswords)
|
||||
Object.keys(config).forEach(key => {
|
||||
if (config[key] !== defaults[key]) {
|
||||
if (key !== 'savePasswords' && config[key] !== defaults[key]) {
|
||||
params.set(key, config[key]);
|
||||
}
|
||||
});
|
||||
@@ -241,19 +351,71 @@ function saveSettings() {
|
||||
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' || key === 'savePasswords') config[key] = value === 'true';
|
||||
key === 'useNumbers' || key === 'useSpecial') config[key] = value === 'true';
|
||||
else config[key] = value;
|
||||
}
|
||||
applyConfig(config);
|
||||
|
||||
// Load savePasswords setting separately from cookies only
|
||||
loadSavePasswordsSetting();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,25 +431,82 @@ function loadSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettingsManual() {
|
||||
// Load savePasswords setting from cookies only (never from URL)
|
||||
function loadSavePasswordsSetting() {
|
||||
const settings = getCookie('passwordGenSettings');
|
||||
if (settings) {
|
||||
try {
|
||||
const config = JSON.parse(decodeURIComponent(settings));
|
||||
applyConfig(config);
|
||||
updateURL();
|
||||
showNotification('Settings loaded successfully!', 'success');
|
||||
} catch (e) {
|
||||
showNotification('Error loading settings: ' + e.message, 'error');
|
||||
}
|
||||
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 {
|
||||
showNotification('No saved settings found.', 'warning');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearSettings() {
|
||||
document.cookie = 'passwordGenSettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
showNotification('Saved settings cleared.', 'info');
|
||||
|
||||
// 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) {
|
||||
@@ -298,7 +517,7 @@ function applyConfig(config) {
|
||||
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('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;
|
||||
@@ -306,16 +525,8 @@ function applyConfig(config) {
|
||||
document.getElementById('numberPosition').value = config.numberPosition || "end";
|
||||
document.getElementById('savePasswords').checked = config.savePasswords || false;
|
||||
|
||||
// Update tab state
|
||||
if (currentMode === 'passphrase') {
|
||||
document.getElementById('passphraseTab').classList.add('active');
|
||||
document.getElementById('randomTab').classList.remove('active');
|
||||
document.getElementById('passphraseControls').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('randomTab').classList.add('active');
|
||||
document.getElementById('passphraseTab').classList.remove('active');
|
||||
document.getElementById('passphraseControls').classList.remove('active');
|
||||
}
|
||||
// Update tab state using the switchTab function to ensure proper styling
|
||||
switchTab(currentMode);
|
||||
}
|
||||
|
||||
// Password history management
|
||||
@@ -376,21 +587,31 @@ function loadPasswordHistory() {
|
||||
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
|
||||
saveSettings();
|
||||
// Auto-save the setting change without notification
|
||||
autoSaveSettings();
|
||||
}
|
||||
|
||||
function displayPasswordHistory(history) {
|
||||
@@ -438,11 +659,16 @@ function copyHistoryPassword(password, index) {
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('Are you sure you want to clear all password history?')) {
|
||||
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();
|
||||
alert('Password history cleared.');
|
||||
showPopup('Password history cleared.', 'info');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Utility function to get cookie value
|
||||
@@ -453,23 +679,16 @@ function getCookie(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load word list info
|
||||
function loadWordListInfo() {
|
||||
fetch('/api/password/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('wordListStatus').innerHTML =
|
||||
`${data.wordCount} words loaded<br>Source: ${data.source}<br>Updated: ${data.lastUpdate}`;
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('wordListStatus').textContent = 'Error loading word list info';
|
||||
});
|
||||
}
|
||||
|
||||
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/password', {
|
||||
fetch('/api/pwgenerator', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -478,35 +697,45 @@ function generatePassword() {
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(password => {
|
||||
document.getElementById('passwordDisplay').textContent = password;
|
||||
document.getElementById('copyBtn').style.display = 'inline-block';
|
||||
document.getElementById('generatedPassword').value = password;
|
||||
|
||||
// Update character count
|
||||
document.getElementById('characterCount').textContent = `${password.length} characters`;
|
||||
|
||||
// Calculate and display strength
|
||||
const strength = calculatePasswordStrength(password);
|
||||
document.getElementById('strengthIndicator').innerHTML = 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('passwordDisplay').textContent = 'Error generating password: ' + error.message;
|
||||
document.getElementById('generatedPassword').value = 'Error generating password';
|
||||
document.getElementById('characterCount').textContent = '0 characters';
|
||||
document.getElementById('strengthDisplay').textContent = 'Error';
|
||||
});
|
||||
}
|
||||
|
||||
function copyPassword() {
|
||||
const password = document.getElementById('passwordDisplay').textContent;
|
||||
if (password && password !== 'Click "Generate Password" to create a secure password') {
|
||||
const password = document.getElementById('generatedPassword').value;
|
||||
if (password && password !== 'Click \'Generate Password\' to create a new password') {
|
||||
navigator.clipboard.writeText(password).then(function() {
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.textContent = 'Copied!';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function() {
|
||||
btn.textContent = 'Copy to Clipboard';
|
||||
btn.classList.remove('copied');
|
||||
}, 2000);
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -538,37 +767,15 @@ function calculatePasswordStrength(password) {
|
||||
else if (score >= 40) { strength = "Moderate"; color = "#ffaa00"; }
|
||||
else if (score >= 20) { strength = "Weak"; color = "#ff8800"; }
|
||||
|
||||
return `<span style="color: ${color}; font-weight: bold;">${strength}</span> (${score}/100)`;
|
||||
return {
|
||||
html: `<span style="color: ${color}; font-weight: bold;">${strength}</span> (${score}/100)`,
|
||||
text: strength
|
||||
};
|
||||
}
|
||||
|
||||
// Notification system
|
||||
// Use the showPopup function from base.html instead of custom notifications
|
||||
function showNotification(message, type = 'info') {
|
||||
// Remove any existing notifications
|
||||
const existingNotifications = document.querySelectorAll('.notification');
|
||||
existingNotifications.forEach(notification => notification.remove());
|
||||
|
||||
// Create new notification
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Show with animation
|
||||
setTimeout(() => {
|
||||
notification.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 5000);
|
||||
showPopup(message, type);
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
774
web/pwgenerator.html
Normal file
774
web/pwgenerator.html
Normal file
@@ -0,0 +1,774 @@
|
||||
{{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>
|
||||
</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>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
|
||||
<div class="flex items-center justify-between mb-4 cursor-pointer" onclick="toggleSettings()">
|
||||
<h3 class="text-xl font-semibold text-gray-200">🔧 Password Settings</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span id="settingsToggleText" class="text-sm text-gray-400">Click to expand</span>
|
||||
<svg id="settingsChevron" class="w-5 h-5 text-gray-400 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settingsContent" class="hidden">
|
||||
<!-- Save Passwords Option -->
|
||||
<div class="p-3 bg-gray-900 rounded-lg border border-gray-600 mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox" id="savePasswords" onchange="togglePasswordSaving(); autoSaveSettings()"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
|
||||
<label for="savePasswords" class="text-sm text-gray-300">
|
||||
Save Generated Passwords (in web browser cookies only)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Table -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Column: Basic Password Settings -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-medium text-gray-200 mb-4 border-b border-gray-600 pb-2">🎲 Random Password Settings</h4>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<label for="minSpecialChars" class="block text-sm font-medium text-gray-300 mb-2">Minimum Special Characters:</label>
|
||||
<input type="number" id="minSpecialChars" min="0" max="10" value="{{.Config.MinSpecialChars}}"
|
||||
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 class="text-gray-500 text-xs mt-1">
|
||||
Minimum number of special characters to include in random passwords
|
||||
</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>
|
||||
|
||||
<!-- Right Column: Passphrase Settings -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-medium text-gray-200 mb-4 border-b border-gray-600 pb-2">📝 Passphrase Options</h4>
|
||||
|
||||
<div id="passphraseControls">
|
||||
<div class="space-y-4">
|
||||
<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(); 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="passphraseUseNumbers" {{if .Config.UseNumbers}}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="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(); 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="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(); 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">
|
||||
<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>
|
||||
</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');
|
||||
}
|
||||
|
||||
updateURL();
|
||||
autoSaveSettings(); // Auto-save without notification
|
||||
}
|
||||
|
||||
document.getElementById('randomTab').addEventListener('click', () => switchTab('random'));
|
||||
document.getElementById('passphraseTab').addEventListener('click', () => switchTab('passphrase'));
|
||||
|
||||
// Toggle settings section
|
||||
function toggleSettings() {
|
||||
const content = document.getElementById('settingsContent');
|
||||
const chevron = document.getElementById('settingsChevron');
|
||||
const toggleText = document.getElementById('settingsToggleText');
|
||||
|
||||
if (content.classList.contains('hidden')) {
|
||||
// Expand
|
||||
content.classList.remove('hidden');
|
||||
chevron.style.transform = 'rotate(180deg)';
|
||||
toggleText.textContent = 'Click to minimize';
|
||||
} else {
|
||||
// Collapse
|
||||
content.classList.add('hidden');
|
||||
chevron.style.transform = 'rotate(0deg)';
|
||||
toggleText.textContent = 'Click to expand';
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
const defaults = {
|
||||
type: "passphrase",
|
||||
length: 12,
|
||||
includeUpper: true,
|
||||
includeLower: true,
|
||||
numberCount: 2,
|
||||
specialChars: "!@#$%&*-_=+.",
|
||||
minSpecialChars: 3,
|
||||
noConsecutive: true,
|
||||
wordCount: 3,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
numberPosition: "end"
|
||||
};
|
||||
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,
|
||||
minSpecialChars: parseInt(document.getElementById('minSpecialChars').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) {
|
||||
// Explicitly exclude savePasswords from URL parameter processing
|
||||
if (key === 'savePasswords') {
|
||||
continue; // Skip this parameter completely
|
||||
}
|
||||
else if (key === 'type') config[key] = value;
|
||||
else if (key === 'length' || key === 'numberCount' || key === 'wordCount' || key === 'minSpecialChars') 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 || 2;
|
||||
document.getElementById('specialChars').value = config.specialChars || "!@#$%&*-_=+.";
|
||||
document.getElementById('minSpecialChars').value = config.minSpecialChars || 3;
|
||||
document.getElementById('noConsecutive').checked = config.noConsecutive || true;
|
||||
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');
|
||||
if (passwordDisplay && passwordDisplay.textContent && passwordDisplay.textContent !== 'Click "Generate Password" to create a secure password') {
|
||||
if (viewHistoryBtn) 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
|
||||
if (viewHistoryBtn) 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`;
|
||||
|
||||
// Add to history
|
||||
addToHistory(password);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
document.getElementById('generatedPassword').value = 'Error generating password';
|
||||
document.getElementById('characterCount').textContent = '0 characters';
|
||||
});
|
||||
}
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Use the showPopup function from base.html instead of custom notifications
|
||||
function showNotification(message, type = 'info') {
|
||||
showPopup(message, type);
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
888
web/pwpush.html
888
web/pwpush.html
File diff suppressed because it is too large
Load Diff
1406
web/style.css
1406
web/style.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user