Add role based login and user management

This commit is contained in:
DonVoo 2026-07-13 00:28:01 +02:00
parent f67bbdce78
commit 353dafc30d
4 changed files with 681 additions and 34 deletions

View file

@ -16,6 +16,10 @@ import (
func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/logout", logoutHandler)
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/users/save", userSaveHandler)
mux.HandleFunc("/users/delete", userDeleteHandler)
// Umzug (Konten pflegen + starten/beobachten)
mux.HandleFunc("/accounts", accountsHandler) // TODO Codex (02/07)
@ -59,16 +63,16 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -81,7 +85,8 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<div class="three-pane">
@ -104,6 +109,9 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
}
func accountsHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
accounts, err := ListAccounts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -122,6 +130,9 @@ func accountsHandler(w http.ResponseWriter, r *http.Request) {
}
func accountSaveHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -151,6 +162,9 @@ func accountSaveHandler(w http.ResponseWriter, r *http.Request) {
}
func accountDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -168,6 +182,9 @@ func accountDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func accountTestHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -185,6 +202,9 @@ func accountTestHandler(w http.ResponseWriter, r *http.Request) {
}
func archivesHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
accounts, err := ListAccounts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -194,6 +214,9 @@ func archivesHandler(w http.ResponseWriter, r *http.Request) {
}
func transferHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
mode := strings.TrimPrefix(r.URL.Path, "/transfer/")
if mode == "" || mode == "/transfer" {
http.Redirect(w, r, "/transfer/postfach-archiv", http.StatusSeeOther)
@ -225,6 +248,9 @@ func emailBoxHandler(w http.ResponseWriter, r *http.Request) {
}
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -242,6 +268,9 @@ func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
}
func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -277,6 +306,9 @@ func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func migrateRunHandler(w http.ResponseWriter, r *http.Request) {
if !requireAdmin(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -322,6 +354,105 @@ func migrateStatusHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `%s: %s %d/%d Fehler %d`, html.EscapeString(j.Account), html.EscapeString(label), j.Done, j.Total, j.Errors)
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
users, err := ListAppUsers()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
editName := strings.TrimSpace(r.URL.Query().Get("edit"))
var edit AppUser
if editName != "" {
edit, err = GetAppUser(editName)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
renderUsersPage(w, r, users, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
}
func userSaveHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
role := strings.TrimSpace(r.FormValue("role"))
active := r.FormValue("active") == "1"
if username == "" {
redirectUsers(w, r, "", "Benutzername fehlt.")
return
}
existing, existingErr := GetAppUser(username)
if !IsAdmin(r) && existingErr == nil && existing.Role != roleUser {
redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins bearbeiten.")
return
}
if !IsAdmin(r) {
role = roleUser
}
if errors.Is(existingErr, sql.ErrNoRows) && password == "" {
redirectUsers(w, r, username, "Passwort fuer neuen Benutzer fehlt.")
return
}
var hash string
if password != "" {
var err error
hash, err = HashPassword(password)
if err != nil {
redirectUsers(w, r, username, err.Error())
return
}
}
if err := SaveAppUser(username, hash, role, active); err != nil {
redirectUsers(w, r, username, err.Error())
return
}
redirectUsers(w, r, username, "Benutzer gespeichert.")
}
func userDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !requireManager(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := strings.TrimSpace(r.FormValue("username"))
if username == "" {
redirectUsers(w, r, "", "Kein Benutzer gewaehlt.")
return
}
current := CurrentUser(r)
if strings.EqualFold(username, current.Username) {
redirectUsers(w, r, username, "Eigenen Benutzer nicht loeschen.")
return
}
target, err := GetAppUser(username)
if err != nil {
redirectUsers(w, r, username, err.Error())
return
}
if !IsAdmin(r) && target.Role != roleUser {
redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins loeschen.")
return
}
if err := DeleteAppUser(username); err != nil {
redirectUsers(w, r, username, err.Error())
return
}
redirectUsers(w, r, "", "Benutzer geloescht.")
}
func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, msg, errMsg string) {
if edit.Name == "" {
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
@ -331,16 +462,16 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Konten-Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -353,7 +484,8 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="accounts-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""))
@ -375,16 +507,16 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -397,7 +529,8 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="accounts-page archive-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""))
@ -414,6 +547,56 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
fmt.Fprint(w, `</div></section></main><footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`)
}
func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, edit AppUser, msg, errMsg string) {
if edit.Username == "" {
edit = AppUser{Role: roleUser, Active: true}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Benutzerverwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab active" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
<aside class="backstage-menu" aria-label="Mail-Graveyard-Menue">
<a class="backstage-item" href="/view">Archiv-Boxen</a>
<a class="backstage-item" href="/view">Import/Export</a>
<a class="backstage-item" href="/accounts">Konten-Verwaltung</a>
<a class="backstage-item" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item" href="/transfer/postfach-archiv">Mail-Transfer</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item active" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="accounts-page users-page">`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""))
if msg != "" {
fmt.Fprintf(w, `<div class="notice ok">%s</div>`, html.EscapeString(msg))
}
if errMsg != "" {
fmt.Fprintf(w, `<div class="notice bad">%s</div>`, html.EscapeString(errMsg))
}
fmt.Fprint(w, `<section class="account-layout"><div class="account-list"><div class="pane-head">Benutzer</div>`)
renderUsersTable(w, users, edit.Username)
fmt.Fprint(w, `</div><div class="account-form-panel"><div class="pane-head">Benutzer eintragen</div>`)
renderUserForm(w, r, edit)
fmt.Fprint(w, `</div></section></main><footer class="statusbar"><span>angemeldet als `)
fmt.Fprint(w, html.EscapeString(CurrentUser(r).Username+" / "+CurrentUser(r).Role))
fmt.Fprint(w, `</span></footer></body></html>`)
}
type transferMode struct {
Slug string
Title string
@ -442,16 +625,16 @@ func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Acc
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -462,7 +645,8 @@ func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Acc
<a class="backstage-item" href="/archives">Archiv-mbox Verwaltung</a>
<a class="backstage-item active" href="/transfer/postfach-archiv">Mail-Transfer</a>
%s
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="accounts-page transfer-page">
@ -482,16 +666,16 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, accounts []Account) {
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quell-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -504,7 +688,8 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, accounts []Account) {
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<div class="three-pane email-box-view">
@ -523,16 +708,16 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, accounts []Account) {
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ziel-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-14">
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-14" defer></script></head>
<script src="/static/app.js?v=20260712-15" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
<nav class="ribbon-tabs">
%s
%s
<a class="tab" href="/accounts">Einstellungen</a>
<a class="tab" href="/users">Einstellungen</a>
</nav>
<div class="ribbon-title">Mail-Graveyard</div>
</header>
@ -545,7 +730,8 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, accounts []Account) {
<a class="backstage-item transfer-sub" href="/transfer/postfach-archiv">Postfach &lt;--&gt; Archiv-MBox</a>
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox &lt;--&gt; Postfach</a>
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach &lt;--&gt; Postfach</a>
<a class="backstage-item" href="/accounts">Einstellungen</a>
<a class="backstage-item" href="/users">Benutzerverwaltung</a>
<a class="backstage-item" href="/logout">Logout</a>
</aside>
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<div class="three-pane email-box-view email-box-target-view">
@ -767,6 +953,70 @@ func renderAccountsTable(w http.ResponseWriter, accounts []Account, editName str
fmt.Fprint(w, `</tbody></table>`)
}
func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) {
if len(users) == 0 {
fmt.Fprint(w, `<div class="ef-empty">Noch keine Benutzer eingetragen.</div>`)
return
}
fmt.Fprint(w, `<table class="account-table user-table"><thead><tr><th>Benutzer</th><th>Rolle</th><th>Status</th><th class="actions-col"></th></tr></thead><tbody>`)
for _, u := range users {
activeClass := ""
if u.Username == editName {
activeClass = ` class="selected"`
}
status := "aktiv"
if !u.Active {
status = "inaktiv"
}
deleteButton := fmt.Sprintf(`<form method="post" action="/users/delete"><input type="hidden" name="username" value="%s"><button class="btn compact secondary danger" type="submit">Loeschen</button></form>`, html.EscapeString(u.Username))
fmt.Fprintf(w, `<tr%s><td><a href="/users?edit=%s">%s</a></td><td>%s</td><td>%s</td><td class="row-actions">%s</td></tr>`,
activeClass, urlQuery(u.Username), html.EscapeString(u.Username), html.EscapeString(u.Role), status, deleteButton)
}
fmt.Fprint(w, `</tbody></table>`)
}
func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) {
roleField := roleSelect(u.Role, IsAdmin(r))
if !IsAdmin(r) {
roleField = `<input type="hidden" name="role" value="user"><input class="input" value="user" disabled>`
}
fmt.Fprintf(w, `<form class="account-form user-form" method="post" action="/users/save">
<div class="form-section">
<label class="label">Benutzername<input class="input" name="username" value="%s" required></label>
<label class="check-row"><input type="checkbox" name="active" value="1" %s> Aktiv</label>
</div>
<div class="form-columns">
<fieldset><legend>Zugang</legend>
<label class="label">Passwort<input class="input" name="password" type="password" autocomplete="new-password" placeholder="%s"></label>
<label class="label">Rolle%s</label>
</fieldset>
</div>
<div class="form-actions">
<button class="btn" type="submit">Speichern</button>
<a class="btn secondary" href="/users">Neu</a>
</div>
</form>`,
html.EscapeString(u.Username), checked(u.Active), passwordPlaceholder(u.Username), roleField)
}
func roleSelect(current string, admin bool) string {
if current == "" {
current = roleUser
}
values := []string{roleUser}
if admin {
values = []string{roleUser, roleManager, roleAdmin}
}
return selectHTML("role", current, values)
}
func passwordPlaceholder(username string) string {
if username == "" {
return "Pflicht fuer neuen Benutzer"
}
return "leer lassen = behalten"
}
func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account) {
if len(archives) == 0 {
fmt.Fprint(w, `<div class="ef-empty">Noch keine Archiv-Mailbox angelegt.</div>`)
@ -1014,6 +1264,18 @@ func redirectArchives(w http.ResponseWriter, r *http.Request, msg string) {
http.Redirect(w, r, "/archives?"+key+"="+urlQuery(msg), http.StatusSeeOther)
}
func redirectUsers(w http.ResponseWriter, r *http.Request, edit, msg string) {
key := "msg"
if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "pflicht") || strings.Contains(strings.ToLower(msg), "nicht") || strings.Contains(strings.ToLower(msg), "error") {
key = "err"
}
target := "/users?" + key + "=" + urlQuery(msg)
if edit != "" {
target += "&edit=" + urlQuery(edit)
}
http.Redirect(w, r, target, http.StatusSeeOther)
}
func urlQuery(value string) string {
return url.QueryEscape(value)
}