Add role based login and user management
This commit is contained in:
parent
f67bbdce78
commit
353dafc30d
4 changed files with 681 additions and 34 deletions
|
|
@ -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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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 <--> Archiv-MBox</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/archiv-postfach">Archiv-MBox <--> Postfach</a>
|
||||
<a class="backstage-item transfer-sub" href="/transfer/postfach-postfach">Postfach <--> 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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue