Improve user management actions
This commit is contained in:
parent
353dafc30d
commit
6e7aa87de6
3 changed files with 88 additions and 19 deletions
|
|
@ -19,6 +19,7 @@ func RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/logout", logoutHandler)
|
mux.HandleFunc("/logout", logoutHandler)
|
||||||
mux.HandleFunc("/users", usersHandler)
|
mux.HandleFunc("/users", usersHandler)
|
||||||
mux.HandleFunc("/users/save", userSaveHandler)
|
mux.HandleFunc("/users/save", userSaveHandler)
|
||||||
|
mux.HandleFunc("/users/toggle-active", userToggleActiveHandler)
|
||||||
mux.HandleFunc("/users/delete", userDeleteHandler)
|
mux.HandleFunc("/users/delete", userDeleteHandler)
|
||||||
|
|
||||||
// Umzug (Konten pflegen + starten/beobachten)
|
// Umzug (Konten pflegen + starten/beobachten)
|
||||||
|
|
@ -63,9 +64,9 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Mail-Graveyard</title>
|
<title>Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -453,6 +454,46 @@ func userDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectUsers(w, r, "", "Benutzer geloescht.")
|
redirectUsers(w, r, "", "Benutzer geloescht.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userToggleActiveHandler(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"))
|
||||||
|
active := r.FormValue("active") == "1"
|
||||||
|
if username == "" {
|
||||||
|
redirectUsers(w, r, "", "Kein Benutzer gewaehlt.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
current := CurrentUser(r)
|
||||||
|
if strings.EqualFold(username, current.Username) && !active {
|
||||||
|
redirectUsers(w, r, username, "Eigenen Benutzer nicht sperren.")
|
||||||
|
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 sperren.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := SaveAppUser(username, "", target.Role, active); err != nil {
|
||||||
|
redirectUsers(w, r, username, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
_, _ = DB.Exec(`DELETE FROM app_sessions WHERE user_id=?`, target.ID)
|
||||||
|
redirectUsers(w, r, username, "Benutzer gesperrt.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redirectUsers(w, r, username, "Benutzer aktiviert.")
|
||||||
|
}
|
||||||
|
|
||||||
func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, msg, errMsg string) {
|
func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, msg, errMsg string) {
|
||||||
if edit.Name == "" {
|
if edit.Name == "" {
|
||||||
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
|
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
|
||||||
|
|
@ -462,9 +503,9 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account,
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Konten-Verwaltung - Mail-Graveyard</title>
|
<title>Konten-Verwaltung - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -507,9 +548,9 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
|
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -555,9 +596,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Benutzerverwaltung - Mail-Graveyard</title>
|
<title>Benutzerverwaltung - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -625,9 +666,9 @@ func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Acc
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>%s - Mail-Graveyard</title>
|
<title>%s - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -666,9 +707,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, accounts []Account) {
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Quell-Postfach - Mail-Graveyard</title>
|
<title>Quell-Postfach - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -708,9 +749,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, accounts []Account) {
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Ziel-Postfach - Mail-Graveyard</title>
|
<title>Ziel-Postfach - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15">
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/app.js?v=20260712-15" defer></script></head>
|
<script src="/static/app.js?v=20260712-16" defer></script></head>
|
||||||
<body class="ol2013">
|
<body class="ol2013">
|
||||||
<header class="ribbon">
|
<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>
|
<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>
|
||||||
|
|
@ -966,11 +1007,21 @@ func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) {
|
||||||
}
|
}
|
||||||
status := "aktiv"
|
status := "aktiv"
|
||||||
if !u.Active {
|
if !u.Active {
|
||||||
status = "inaktiv"
|
status = "gesperrt"
|
||||||
}
|
}
|
||||||
|
nextActive := "0"
|
||||||
|
toggleLabel := "Sperren"
|
||||||
|
toggleClass := "btn compact secondary"
|
||||||
|
if !u.Active {
|
||||||
|
nextActive = "1"
|
||||||
|
toggleLabel = "Aktivieren"
|
||||||
|
}
|
||||||
|
editButton := fmt.Sprintf(`<a class="btn compact secondary" href="/users?edit=%s">Bearbeiten</a>`, urlQuery(u.Username))
|
||||||
|
toggleButton := fmt.Sprintf(`<form method="post" action="/users/toggle-active"><input type="hidden" name="username" value="%s"><input type="hidden" name="active" value="%s"><button class="%s" type="submit">%s</button></form>`,
|
||||||
|
html.EscapeString(u.Username), nextActive, toggleClass, toggleLabel)
|
||||||
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))
|
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>`,
|
fmt.Fprintf(w, `<tr%s><td>%s</td><td>%s</td><td>%s</td><td class="row-actions">%s%s%s</td></tr>`,
|
||||||
activeClass, urlQuery(u.Username), html.EscapeString(u.Username), html.EscapeString(u.Role), status, deleteButton)
|
activeClass, html.EscapeString(u.Username), html.EscapeString(u.Role), status, editButton, toggleButton, deleteButton)
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, `</tbody></table>`)
|
fmt.Fprint(w, `</tbody></table>`)
|
||||||
}
|
}
|
||||||
|
|
@ -987,6 +1038,7 @@ func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) {
|
||||||
</div>
|
</div>
|
||||||
<div class="form-columns">
|
<div class="form-columns">
|
||||||
<fieldset><legend>Zugang</legend>
|
<fieldset><legend>Zugang</legend>
|
||||||
|
<div class="form-hint">%s</div>
|
||||||
<label class="label">Passwort<input class="input" name="password" type="password" autocomplete="new-password" placeholder="%s"></label>
|
<label class="label">Passwort<input class="input" name="password" type="password" autocomplete="new-password" placeholder="%s"></label>
|
||||||
<label class="label">Rolle%s</label>
|
<label class="label">Rolle%s</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -996,7 +1048,7 @@ func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) {
|
||||||
<a class="btn secondary" href="/users">Neu</a>
|
<a class="btn secondary" href="/users">Neu</a>
|
||||||
</div>
|
</div>
|
||||||
</form>`,
|
</form>`,
|
||||||
html.EscapeString(u.Username), checked(u.Active), passwordPlaceholder(u.Username), roleField)
|
html.EscapeString(u.Username), checked(u.Active), passwordHint(u.Username), passwordPlaceholder(u.Username), roleField)
|
||||||
}
|
}
|
||||||
|
|
||||||
func roleSelect(current string, admin bool) string {
|
func roleSelect(current string, admin bool) string {
|
||||||
|
|
@ -1017,6 +1069,13 @@ func passwordPlaceholder(username string) string {
|
||||||
return "leer lassen = behalten"
|
return "leer lassen = behalten"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func passwordHint(username string) string {
|
||||||
|
if username == "" {
|
||||||
|
return "Neuer Benutzer: Passwort eintragen."
|
||||||
|
}
|
||||||
|
return "Bearbeiten: Neues Passwort eintragen oder leer lassen, um das bestehende Passwort zu behalten."
|
||||||
|
}
|
||||||
|
|
||||||
func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account) {
|
func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account) {
|
||||||
if len(archives) == 0 {
|
if len(archives) == 0 {
|
||||||
fmt.Fprint(w, `<div class="ef-empty">Noch keine Archiv-Mailbox angelegt.</div>`)
|
fmt.Fprint(w, `<div class="ef-empty">Noch keine Archiv-Mailbox angelegt.</div>`)
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ func renderLoginPage(w http.ResponseWriter, errMsg string) {
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta 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">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Login - Mail-Graveyard</title>
|
<title>Login - Mail-Graveyard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=20260712-15"></head>
|
<link rel="stylesheet" href="/static/style.css?v=20260712-16"></head>
|
||||||
<body class="ol2013 login-body">
|
<body class="ol2013 login-body">
|
||||||
<main class="login-card">
|
<main class="login-card">
|
||||||
<div class="login-brand"><span class="mail-logo" aria-hidden="true"></span><strong>Mail-Graveyard</strong></div>
|
<div class="login-brand"><span class="mail-logo" aria-hidden="true"></span><strong>Mail-Graveyard</strong></div>
|
||||||
|
|
|
||||||
10
frontend-js/dist/style.css
vendored
10
frontend-js/dist/style.css
vendored
|
|
@ -490,6 +490,11 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
||||||
min-width: 204px;
|
min-width: 204px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
.user-table .actions-col,
|
||||||
|
.user-table .row-actions {
|
||||||
|
width: 292px;
|
||||||
|
min-width: 292px;
|
||||||
|
}
|
||||||
.account-table th {
|
.account-table th {
|
||||||
color: var(--ol-muted);
|
color: var(--ol-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -550,6 +555,11 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
||||||
}
|
}
|
||||||
.form-actions a { text-decoration: none; }
|
.form-actions a { text-decoration: none; }
|
||||||
.btn.danger { color: #8a1f11; }
|
.btn.danger { color: #8a1f11; }
|
||||||
|
.form-hint {
|
||||||
|
color: var(--ol-muted);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
.archive-table {
|
.archive-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue