621 lines
20 KiB
Go
621 lines
20 KiB
Go
// main.go
|
||
package main
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html"
|
||
"log"
|
||
"math/big"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
type Config struct {
|
||
Addr string
|
||
MinLength int
|
||
MinLower int
|
||
MinUpper int
|
||
MinDigits int
|
||
MinSpecial int
|
||
Specials string
|
||
LeetMap map[rune]string
|
||
Suggestions int
|
||
RuleSets []string // e.g. ["random","passphrase","hybrid"]
|
||
LeetProb float64
|
||
}
|
||
|
||
type genRequest struct {
|
||
Sentence string `json:"sentence"`
|
||
}
|
||
|
||
type complexity struct {
|
||
Lower int `json:"lower"`
|
||
Upper int `json:"upper"`
|
||
Digits int `json:"digits"`
|
||
Special int `json:"special"`
|
||
Length int `json:"length"`
|
||
}
|
||
|
||
type genResponse struct {
|
||
Password string `json:"password"`
|
||
Complexity complexity `json:"complexity"`
|
||
Ok bool `json:"ok"`
|
||
Message string `json:"message,omitempty"`
|
||
}
|
||
|
||
func main() {
|
||
cfg := loadConfig()
|
||
|
||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
fmt.Fprint(w, indexHTML(cfg))
|
||
})
|
||
|
||
http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
var req genRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeJSON(w, genResponse{Ok: false, Message: "Ungültige Eingabe"})
|
||
return
|
||
}
|
||
pw, cplx := generateFromSentence(strings.TrimSpace(req.Sentence), cfg)
|
||
ok := meets(cfg, cplx)
|
||
writeJSON(w, genResponse{Ok: ok, Password: pw, Complexity: cplx})
|
||
})
|
||
|
||
http.HandleFunc("/api/suggest", func(w http.ResponseWriter, r *http.Request) {
|
||
// Gibt drei+ Vorschläge aus mindestens 3 RuleSets zurück
|
||
out := struct {
|
||
Suggestions []string `json:"suggestions"`
|
||
}{Suggestions: suggestions(cfg)}
|
||
writeJSON(w, out)
|
||
})
|
||
|
||
log.Printf("Listening on %s ...", cfg.Addr)
|
||
if err := http.ListenAndServe(cfg.Addr, nil); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
}
|
||
|
||
/* -------------------- Config & ENV -------------------- */
|
||
|
||
func floatEnv(k string, def float64) float64 {
|
||
if v := os.Getenv(k); v != "" {
|
||
v = strings.ReplaceAll(v, ",", ".") // deutsche Kommas erlauben
|
||
if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
|
||
return f
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func randChance(p float64) bool {
|
||
if p <= 0 { return false }
|
||
if p >= 1 { return true }
|
||
// 1e6 Auflösung reicht; kryptografisch sicher über mustCryptoInt
|
||
return mustCryptoInt(1_000_000) < int(p*1_000_000)
|
||
}
|
||
|
||
func loadConfig() Config {
|
||
minLen := intEnv("MIN_LENGTH", 12)
|
||
minLower := intEnv("MIN_LOWER", 2)
|
||
minUpper := intEnv("MIN_UPPER", 2)
|
||
minDigits := intEnv("MIN_DIGITS", 2)
|
||
minSpecial := intEnv("MIN_SPECIAL", 2)
|
||
addr := strEnv("SERVER_ADDR", ":8080")
|
||
specials := strEnv("SPECIALS", "!@#$%&*()-=+:?")
|
||
leet := parseLeetMap(strEnv("LEET_MAP", "a:4,e:3,o:0,t:7"))
|
||
leetProb := floatEnv("LEET_PROB", 0.35)
|
||
sug := intEnv("SUGGESTION_COUNT", 5)
|
||
rules := strEnv("RULESETS", "random,passphrase,hybrid")
|
||
rs := []string{}
|
||
for _, p := range strings.Split(rules, ",") {
|
||
p = strings.TrimSpace(p)
|
||
if p != "" {
|
||
rs = append(rs, p)
|
||
}
|
||
}
|
||
if len(rs) == 0 {
|
||
rs = []string{"random", "passphrase", "hybrid"}
|
||
}
|
||
return Config{
|
||
Addr: addr,
|
||
MinLength: minLen,
|
||
MinLower: minLower,
|
||
MinUpper: minUpper,
|
||
MinDigits: minDigits,
|
||
MinSpecial: minSpecial,
|
||
Specials: specials,
|
||
LeetMap: leet,
|
||
Suggestions: sug,
|
||
RuleSets: rs,
|
||
LeetProb: leetProb,
|
||
}
|
||
}
|
||
|
||
func strEnv(k, def string) string {
|
||
if v := os.Getenv(k); v != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|
||
|
||
func intEnv(k string, def int) int {
|
||
if v := os.Getenv(k); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil {
|
||
return n
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func parseLeetMap(s string) map[rune]string {
|
||
// Format: "a:4,e:3,o:0,t:7"
|
||
out := map[rune]string{}
|
||
for _, pair := range strings.Split(s, ",") {
|
||
p := strings.Split(strings.TrimSpace(pair), ":")
|
||
if len(p) == 2 && len(p[0]) == 1 {
|
||
k := []rune(strings.ToLower(p[0]))[0]
|
||
out[k] = p[1]
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/* -------------------- Core Generation -------------------- */
|
||
|
||
func generateFromSentence(sentence string, cfg Config) (string, complexity) {
|
||
if sentence == "" {
|
||
// Leerer Input -> generiere einen Vorschlag nach "hybrid"
|
||
pw := genHybrid(cfg)
|
||
return ensureAll(pw, cfg)
|
||
}
|
||
// 1) Normalisieren & Leet
|
||
words := strings.Fields(sentence)
|
||
if len(words) == 0 {
|
||
pw := genHybrid(cfg)
|
||
return ensureAll(pw, cfg)
|
||
}
|
||
// ersetze Leerzeichen durch zufällige Sonderzeichen
|
||
var b strings.Builder
|
||
for i, w := range words {
|
||
if i > 0 {
|
||
b.WriteString(randFrom(cfg.Specials))
|
||
}
|
||
b.WriteString(applyLeet(w, cfg.LeetMap, cfg.LeetProb))
|
||
}
|
||
pw := b.String()
|
||
|
||
// 2) Zufällige Groß-/Kleinschreibung einstreuen
|
||
pw = sprinkleCase(pw)
|
||
|
||
// 3) Komplexität & Mindestlänge sicherstellen
|
||
return ensureAll(pw, cfg)
|
||
}
|
||
|
||
func applyLeet(s string, leet map[rune]string, p float64) string {
|
||
var b strings.Builder
|
||
for _, r := range s {
|
||
lr := unicode.ToLower(r)
|
||
if rep, ok := leet[lr]; ok && randChance(p) {
|
||
b.WriteString(rep) // Ersetze nur mit Chance p
|
||
} else {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func sprinkleCase(s string) string {
|
||
rs := []rune(s)
|
||
for i, r := range rs {
|
||
if unicode.IsLetter(r) {
|
||
// ~50% Chance flip case
|
||
if coin() {
|
||
if unicode.IsUpper(r) {
|
||
rs[i] = unicode.ToLower(r)
|
||
} else {
|
||
rs[i] = unicode.ToUpper(r)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return string(rs)
|
||
}
|
||
|
||
func ensureAll(pw string, cfg Config) (string, complexity) {
|
||
// Schrittweise Komplexität erzwingen, dann Mindestlänge
|
||
pw, c := ensureComplexity(pw, cfg)
|
||
if runeLen(pw) < cfg.MinLength {
|
||
need := cfg.MinLength - runeLen(pw)
|
||
allSet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials
|
||
for i := 0; i < need; i++ {
|
||
pw = insertAt(pw, randPos(pw), randFrom(allSet))
|
||
}
|
||
c = countComplexity(pw, cfg)
|
||
}
|
||
return pw, c
|
||
}
|
||
|
||
func ensureComplexity(s string, cfg Config) (string, complexity) {
|
||
pw := s
|
||
c := countComplexity(pw, cfg)
|
||
|
||
// Ziffern
|
||
for c.Digits < cfg.MinDigits {
|
||
pw = insertAt(pw, randPos(pw), randFrom("0123456789"))
|
||
c = countComplexity(pw, cfg)
|
||
}
|
||
// Sonderzeichen
|
||
for c.Special < cfg.MinSpecial {
|
||
pw = insertAt(pw, randPos(pw), randFrom(cfg.Specials))
|
||
c = countComplexity(pw, cfg)
|
||
}
|
||
// Großbuchstaben
|
||
for c.Upper < cfg.MinUpper {
|
||
if i := findIndexLetter(pw, false); i >= 0 { // irgendein Buchstabe
|
||
pw = setCaseAt(pw, i, true)
|
||
} else {
|
||
pw = insertAt(pw, randPos(pw), randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
|
||
}
|
||
c = countComplexity(pw, cfg)
|
||
}
|
||
// Kleinbuchstaben
|
||
for c.Lower < cfg.MinLower {
|
||
if i := findIndexLetter(pw, false); i >= 0 {
|
||
pw = setCaseAt(pw, i, false)
|
||
} else {
|
||
pw = insertAt(pw, randPos(pw), randFrom("abcdefghijklmnopqrstuvwxyz"))
|
||
}
|
||
c = countComplexity(pw, cfg)
|
||
}
|
||
return pw, c
|
||
}
|
||
|
||
func countComplexity(s string, cfg Config) complexity {
|
||
var c complexity
|
||
for _, r := range s {
|
||
switch {
|
||
case unicode.IsLower(r):
|
||
c.Lower++
|
||
case unicode.IsUpper(r):
|
||
c.Upper++
|
||
case unicode.IsDigit(r):
|
||
c.Digits++
|
||
default:
|
||
if strings.ContainsRune(cfg.Specials, r) {
|
||
c.Special++
|
||
}
|
||
}
|
||
}
|
||
c.Length = runeLen(s)
|
||
return c
|
||
}
|
||
|
||
func meets(cfg Config, c complexity) bool {
|
||
return c.Length >= cfg.MinLength &&
|
||
c.Lower >= cfg.MinLower &&
|
||
c.Upper >= cfg.MinUpper &&
|
||
c.Digits >= cfg.MinDigits &&
|
||
c.Special >= cfg.MinSpecial
|
||
}
|
||
|
||
/* -------------------- Suggestions (3+ RuleSets) -------------------- */
|
||
|
||
func suggestions(cfg Config) []string {
|
||
rs := []string{}
|
||
// Mindestens eins je RuleSet
|
||
for _, set := range cfg.RuleSets {
|
||
switch set {
|
||
case "random":
|
||
rs = append(rs, genRandom(cfg))
|
||
case "passphrase":
|
||
rs = append(rs, genPassphrase(cfg))
|
||
case "hybrid":
|
||
rs = append(rs, genHybrid(cfg))
|
||
}
|
||
}
|
||
// Falls mehr gewünscht, fülle gemischt auf
|
||
for len(rs) < cfg.Suggestions {
|
||
switch randPick([]string{"random", "passphrase", "hybrid"}) {
|
||
case "random":
|
||
rs = append(rs, genRandom(cfg))
|
||
case "passphrase":
|
||
rs = append(rs, genPassphrase(cfg))
|
||
default:
|
||
rs = append(rs, genHybrid(cfg))
|
||
}
|
||
}
|
||
return rs
|
||
}
|
||
|
||
func genRandom(cfg Config) string {
|
||
all := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials
|
||
// Länge: mindestens MinLength bis MinLength+6
|
||
L := cfg.MinLength + mustCryptoInt(5) + 2 // +2..+6
|
||
var b strings.Builder
|
||
for i := 0; i < L; i++ {
|
||
b.WriteString(randFrom(all))
|
||
}
|
||
pw, _ := ensureAll(b.String(), cfg)
|
||
return pw
|
||
}
|
||
|
||
func genPassphrase(cfg Config) string {
|
||
// 3–4 Wörter, zufällige Trenner, zufällige Großschreibung & Leet-sprinkles
|
||
n := 3 + mustCryptoInt(2) // 3..4
|
||
parts := make([]string, 0, n)
|
||
for i := 0; i < n; i++ {
|
||
w := wordList[mustCryptoInt(len(wordList))]
|
||
if coin() {
|
||
w = strings.ToUpper(w[:1]) + w[1:]
|
||
}
|
||
// gelegentlich Leet auf einzelne Buchstaben
|
||
if coin() && len(w) > 1 {
|
||
idx := mustCryptoInt(len(w))
|
||
r := []rune(w)
|
||
lr := unicode.ToLower(r[idx])
|
||
if rep, ok := cfg.LeetMap[lr]; ok {
|
||
r[idx] = []rune(rep)[0]
|
||
w = string(r)
|
||
}
|
||
}
|
||
parts = append(parts, w)
|
||
}
|
||
sep := randFrom(cfg.Specials)
|
||
pw := strings.Join(parts, sep)
|
||
// Zahlen zufällig einsprenkeln
|
||
if coin() {
|
||
pw = insertAt(pw, randPos(pw), strconv.Itoa(10+mustCryptoInt(89)))
|
||
}
|
||
pw, _ = ensureAll(pw, cfg)
|
||
return pw
|
||
}
|
||
|
||
func genHybrid(cfg Config) string {
|
||
// 2 Wörter + 2 Sonderzeichen + 3–5 Ziffern gemischt
|
||
w1 := wordList[mustCryptoInt(len(wordList))]
|
||
w2 := wordList[mustCryptoInt(len(wordList))]
|
||
if coin() {
|
||
w1 = strings.ToUpper(w1[:1]) + w1[1:]
|
||
}
|
||
if coin() {
|
||
w2 = strings.ToUpper(w2[:1]) + w2[1:]
|
||
}
|
||
base := applyLeet(w1, cfg.LeetMap, cfg.LeetProb) + randFrom(cfg.Specials) + applyLeet(w2, cfg.LeetMap, cfg.LeetProb)
|
||
// streue zweite Sonderzeichen
|
||
base = insertAt(base, randPos(base), randFrom(cfg.Specials))
|
||
// 3–5 Ziffern an zufälligen Stellen
|
||
numN := 3 + mustCryptoInt(3)
|
||
for i := 0; i < numN; i++ {
|
||
base = insertAt(base, randPos(base), randFrom("0123456789"))
|
||
}
|
||
pw, _ := ensureAll(base, cfg)
|
||
return pw
|
||
}
|
||
|
||
/* -------------------- Utilities -------------------- */
|
||
|
||
func writeJSON(w http.ResponseWriter, v any) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
_ = json.NewEncoder(w).Encode(v)
|
||
}
|
||
|
||
func runeLen(s string) int { return len([]rune(s)) }
|
||
|
||
func randFrom(pool string) string {
|
||
i := mustCryptoInt(len(pool))
|
||
return string(pool[i])
|
||
}
|
||
|
||
func randPos(s string) int {
|
||
L := runeLen(s)
|
||
if L == 0 {
|
||
return 0
|
||
}
|
||
return mustCryptoInt(L + 1) // erlauben "ans Ende"
|
||
}
|
||
|
||
func insertAt(s string, pos int, piece string) string {
|
||
rs := []rune(s)
|
||
if pos <= 0 {
|
||
return piece + s
|
||
}
|
||
if pos >= len(rs) {
|
||
return s + piece
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString(string(rs[:pos]))
|
||
b.WriteString(piece)
|
||
b.WriteString(string(rs[pos:]))
|
||
return b.String()
|
||
}
|
||
|
||
func findIndexLetter(s string, onlyLower bool) int {
|
||
rs := []rune(s)
|
||
// Zufällige Startposition, um Muster zu vermeiden
|
||
start := mustCryptoInt(len(rs) + 1)
|
||
for i := 0; i < len(rs); i++ {
|
||
idx := (start + i) % len(rs)
|
||
r := rs[idx]
|
||
if unicode.IsLetter(r) {
|
||
if onlyLower {
|
||
if unicode.IsLower(r) {
|
||
return idx
|
||
}
|
||
} else {
|
||
return idx
|
||
}
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func setCaseAt(s string, idx int, upper bool) string {
|
||
rs := []rune(s)
|
||
if idx < 0 || idx >= len(rs) {
|
||
return s
|
||
}
|
||
if !unicode.IsLetter(rs[idx]) {
|
||
// wenn keine Letter -> füge stattdessen einen passenden Buchstaben ein
|
||
if upper {
|
||
return insertAt(s, idx, randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
|
||
}
|
||
return insertAt(s, idx, randFrom("abcdefghijklmnopqrstuvwxyz"))
|
||
}
|
||
if upper {
|
||
rs[idx] = unicode.ToUpper(rs[idx])
|
||
} else {
|
||
rs[idx] = unicode.ToLower(rs[idx])
|
||
}
|
||
return string(rs)
|
||
}
|
||
|
||
func coin() bool { return mustCryptoInt(2) == 0 }
|
||
|
||
func mustCryptoInt(max int) int {
|
||
if max <= 1 {
|
||
return 0
|
||
}
|
||
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return int(nBig.Int64())
|
||
}
|
||
|
||
func randPick(options []string) string {
|
||
return options[mustCryptoInt(len(options))]
|
||
}
|
||
|
||
/* -------------------- Minimal UI (alles lokal) -------------------- */
|
||
|
||
func indexHTML(cfg Config) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("<!doctype html>\n")
|
||
b.WriteString("<html lang='de'>\n<head>\n<meta charset='utf-8'>\n<meta name='viewport' content='width=device-width,initial-scale=1'>\n")
|
||
b.WriteString("<title>Password Forge (Go)</title>\n<style>\n")
|
||
b.WriteString(":root{--bg:#0b0f14;--card:#111827;--ink:#e5e7eb;--muted:#9ca3af;--accent:#60a5fa;--ok:#34d399;--bad:#f87171}\n")
|
||
b.WriteString("*{box-sizing:border-box}\n")
|
||
b.WriteString("body{margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}\n")
|
||
b.WriteString(".container{max-width:880px;margin:32px auto;padding:0 16px}\n")
|
||
b.WriteString(".card{background:var(--card);border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.35);padding:20px}\n")
|
||
b.WriteString("h1{margin:0 0 8px;font-size:24px}\n")
|
||
b.WriteString("p.sub{margin:0 0 18px;color:var(--muted)}\n")
|
||
b.WriteString("label{display:block;font-size:14px;color:var(--muted);margin-bottom:6px}\n")
|
||
b.WriteString("textarea{width:100%;min-height:90px;border:1px solid #1f2937;background:#0f1622;color:var(--ink);border-radius:12px;padding:12px;resize:vertical}\n")
|
||
b.WriteString("button{cursor:pointer;border:0;border-radius:12px;padding:10px 14px;background:var(--accent);color:#0b0f14;font-weight:600}\n")
|
||
b.WriteString(".row{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap}\n")
|
||
b.WriteString(".output{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;background:#0f1622;color:#e2e8f0;border-radius:12px;padding:12px;word-break:break-all}\n")
|
||
b.WriteString(".grid{display:grid;grid-template-columns:1fr;gap:12px}\n")
|
||
b.WriteString("@media(min-width:800px){.grid{grid-template-columns:2fr 1fr}}\n")
|
||
b.WriteString(".badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;background:#0f1622;padding:6px 10px;font-size:12px;color:var(--muted)}\n")
|
||
b.WriteString(".badge.ok{color:var(--ok)}\n")
|
||
b.WriteString(".badge.bad{color:var(--bad)}\n")
|
||
b.WriteString("small.kv{color:var(--muted);display:block}\n")
|
||
b.WriteString(".suggestion{padding:10px;border:1px dashed #283243;border-radius:10px;background:#0f1622;word-break:break-all}\n")
|
||
b.WriteString("kbd{background:#111827;border:1px solid #223046;border-radius:6px;padding:3px 6px}\n")
|
||
b.WriteString("footer{opacity:.7;margin-top:20px;font-size:12px}\n")
|
||
b.WriteString("</style>\n</head>\n<body>\n")
|
||
b.WriteString("<div class='container'>\n <div class='card'>\n <h1>🔐 Password Forge</h1>\n")
|
||
b.WriteString(" <p class='sub'>Aus einem Satz ein starkes Kennwort erzeugen - alternativ geben Sie einzelne Wörter ein.</p>\n")
|
||
b.WriteString(" <div class='grid'>\n <div>\n <label for='sentence'>Eingabesatz</label>\n")
|
||
b.WriteString(" <textarea id='sentence' placeholder='Z.B. “Morgen ist schönes Wetter”'></textarea>\n")
|
||
b.WriteString(" <div class='row' style='margin-top:10px'>\n")
|
||
b.WriteString(" <button id='btnGen'>Kennwort erzeugen</button>\n")
|
||
b.WriteString(" <span id='status' class='badge'>Mindestlänge: ")
|
||
b.WriteString(strconv.Itoa(cfg.MinLength))
|
||
b.WriteString(" • lower≥")
|
||
b.WriteString(strconv.Itoa(cfg.MinLower))
|
||
b.WriteString(" upper≥")
|
||
b.WriteString(strconv.Itoa(cfg.MinUpper))
|
||
b.WriteString(" digits≥")
|
||
b.WriteString(strconv.Itoa(cfg.MinDigits))
|
||
b.WriteString(" special≥")
|
||
b.WriteString(strconv.Itoa(cfg.MinSpecial))
|
||
b.WriteString("</span>\n")
|
||
b.WriteString(" </div>\n <div style='margin-top:12px'>\n <label>Ergebnis</label>\n")
|
||
b.WriteString(" <div id='out' class='output'>–</div>\n <small id='cplx' class='kv'></small>\n")
|
||
b.WriteString(" </div>\n </div>\n <div>\n")
|
||
b.WriteString(" <div style='display:flex;justify-content:space-between;align-items:center'>\n")
|
||
b.WriteString(" <label>Zufällige Vorschläge</label>\n")
|
||
b.WriteString(" <button id='btnRefresh' style='background:#38bdf8'>Neu</button>\n")
|
||
b.WriteString(" </div>\n")
|
||
b.WriteString(" <div id='sugs' style='display:grid;gap:8px'></div>\n")
|
||
b.WriteString(" </div>\n </div>\n <footer>\n Running on <kbd>")
|
||
b.WriteString(html.EscapeString(cfg.Addr))
|
||
b.WriteString("</kbd> • ENV: <kbd>MIN_LENGTH=")
|
||
b.WriteString(strconv.Itoa(cfg.MinLength))
|
||
b.WriteString("</kbd> <kbd>MIN_LOWER=")
|
||
b.WriteString(strconv.Itoa(cfg.MinLower))
|
||
b.WriteString("</kbd> <kbd>MIN_UPPER=")
|
||
b.WriteString(strconv.Itoa(cfg.MinUpper))
|
||
b.WriteString("</kbd> <kbd>MIN_DIGITS=")
|
||
b.WriteString(strconv.Itoa(cfg.MinDigits))
|
||
b.WriteString("</kbd> <kbd>MIN_SPECIAL=")
|
||
b.WriteString(strconv.Itoa(cfg.MinSpecial))
|
||
b.WriteString("</kbd>\n")
|
||
b.WriteString(" </footer>\n </div>\n</div>\n<script>\n")
|
||
b.WriteString("const $ = function(sel){ return document.querySelector(sel); };\n")
|
||
b.WriteString("function renderComplexity(c, ok){\n")
|
||
b.WriteString(" const badge = ok ? 'badge ok' : 'badge bad';\n")
|
||
b.WriteString(" return '<span class=\"' + badge + '\">L:' + c.length + '</span> '\n")
|
||
b.WriteString(" + '<span class=\"' + badge + '\">a:' + c.lower + '</span> '\n")
|
||
b.WriteString(" + '<span class=\"' + badge + '\">A:' + c.upper + '</span> '\n")
|
||
b.WriteString(" + '<span class=\"' + badge + '\">0:' + c.digits + '</span> '\n")
|
||
b.WriteString(" + '<span class=\"' + badge + '\">#:' + c.special + '</span>'; }\n")
|
||
b.WriteString("async function postJSON(url, data){\n")
|
||
b.WriteString(" const res = await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n")
|
||
b.WriteString(" return await res.json(); }\n")
|
||
b.WriteString("async function getJSON(url){ const res = await fetch(url); return await res.json(); }\n")
|
||
b.WriteString("async function generate(){\n")
|
||
b.WriteString(" const sentence = $('#sentence').value;\n")
|
||
b.WriteString(" const res = await postJSON('/api/generate', { sentence });\n")
|
||
b.WriteString(" if(res && res.password){ $('#out').textContent = res.password; $('#cplx').innerHTML = renderComplexity(res.complexity, res.ok); }\n")
|
||
b.WriteString(" else { $('#out').textContent = res.message || 'Fehler'; $('#cplx').textContent = ''; } }\n")
|
||
b.WriteString("async function loadSugs(){\n")
|
||
b.WriteString(" const data = await getJSON('/api/suggest');\n")
|
||
b.WriteString(" const suggestions = (data && data.suggestions) ? data.suggestions : [];\n")
|
||
b.WriteString(" const wrap = $('#sugs'); wrap.innerHTML = '';\n")
|
||
b.WriteString(" suggestions.forEach(function(s){ const d = document.createElement('div'); d.className='suggestion'; d.textContent = s; wrap.appendChild(d); }); }\n")
|
||
b.WriteString("$('#btnGen').addEventListener('click', generate);\n")
|
||
b.WriteString("$('#btnRefresh').addEventListener('click', loadSugs);\n")
|
||
b.WriteString("loadSugs();\n")
|
||
b.WriteString("</script>\n</body>\n</html>\n")
|
||
|
||
return b.String()
|
||
}
|
||
|
||
/* -------------------- Small built-in word list -------------------- */
|
||
|
||
var wordList = []string{
|
||
"amt","behörde","rathaus","kreisamt","meldeamt","bauamt","forstamt","jagdamt","kasse","haushalt",
|
||
"doppik","kameral","konto","buchung","beleg","rechnung","revision","vergabe","gesetz","satzung",
|
||
"erlass","bescheid","antrag","auflage","ermessen","frist","recht","klage","einwand","widerruf",
|
||
"freigabe","lizenz","akte","akten","postfach","stempel","vorlage","signatur","eakte","dms",
|
||
"vorgang","ablage","archiv","ozg","eid","ifg","dsgvo","bsi","egov","portal",
|
||
"leitweg","peppol","vpn","backup","rollen","rechte","audit","token","tls","rat",
|
||
"stadtrat","kreistag","landtag","senat","wahl","wahlamt","mandat","sitzung","gebühr","kosten",
|
||
"ordnung","bußgeld","einsatz","streife","polizei","zoll","rettung","lage","sirene","nina",
|
||
"thw","verkehr","öpnv","fahrplan","tarif","ticket","fahrzeug","zeugnis","bauplan","planung",
|
||
"abnahme","denkmal","forst","wasser","abfall","müll","klärwerk","luftplan","lärm","energie",
|
||
"steuer","einkauf","umsatz","mahnung","sperre","vermerk","mittel","zuschuss","projekt","bericht",
|
||
"kennzahl","ziel","qualität","schulung","barriere","feedback","hotline","service","presse","bekannt",
|
||
"zustell","zwang","treuhand","amtsweg","hauspost","intranet","aufsicht","referat","dezernat","bereich",
|
||
"stelle","termin","formular","zahlung","automat","opendata","abfrage","meldung","gewerbe","parken",
|
||
"ausweis","urkunde","pass","schein","bafög","wohngeld","schule","kita","kiga","jugend",
|
||
"hilfe","pflege","rente","familie","arbeit","agentur","asyl","visum","titel","status",
|
||
}
|
||
|
||
|
||
/* -------------------- END -------------------- */
|