test
This commit is contained in:
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module git.send.nrw/sendnrw/passwordsuggester
|
||||
|
||||
go 1.25.1
|
||||
590
main.go
Normal file
590
main.go
Normal file
@@ -0,0 +1,590 @@
|
||||
// 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"]
|
||||
}
|
||||
|
||||
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 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", ":8081")
|
||||
specials := strEnv("SPECIALS", "!@#$%^&*()-_=+[]{};:,.?/")
|
||||
leet := parseLeetMap(strEnv("LEET_MAP", "a:4,e:3,o:0,t:7"))
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
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) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
lr := unicode.ToLower(r)
|
||||
if rep, ok := leet[lr]; ok {
|
||||
b.WriteString(rep)
|
||||
} 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) + randFrom(cfg.Specials) + applyLeet(w2, cfg.LeetMap)
|
||||
// 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 – mit konfigurierbaren Regeln (alles lokal).</p>\n")
|
||||
b.WriteString(" <div class='grid'>\n <div>\n <label for='sentence'>Eingabesatz</label>\n")
|
||||
b.WriteString(" <textarea id='sentence' placeholder='Z.B. “Mein erster Urlaub war 2012 in Porto!”'></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{
|
||||
"apfel","atlas","anker","balou","blitz","brise","cobalt","cobra","dialog","dorado","dampf","edel","eule",
|
||||
"falke","fjord","funk","galaxy","garnet","geist","harbor","herz","honig","indigo","ion","juno","jade",
|
||||
"kiesel","komet","kobalt","lotus","lava","lynx","meteor","moos","nimbus","neon","novum","opal","orbit",
|
||||
"pixel","polaris","quarz","quell","raven","regen","rythm","saphir","sigma","silber","tau","titan","topas",
|
||||
"umbra","union","vektor","violett","vulkan","wolke","wogen","xenon","yukon","zenit","zeder",
|
||||
"amber","boreal","cedar","delta","ember","fable","gamma","helios","iris","jelly","kappa","lemur","magma",
|
||||
"nova","onyx","plume","quince","ridge","spruce","terra","ultra","vivid","willow","xerox","yodel","zesty",
|
||||
}
|
||||
|
||||
/* -------------------- END -------------------- */
|
||||
Reference in New Issue
Block a user