diff --git a/backend/00-router.go b/backend/00-router.go index bdd6818..2d3f4db 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -64,9 +64,9 @@ func renderShell(w http.ResponseWriter, active, readingPane string) { fmt.Fprintf(w, ` Mail-Graveyard - + - +
@@ -385,6 +385,7 @@ func userSaveHandler(w http.ResponseWriter, r *http.Request) { return } username := strings.TrimSpace(r.FormValue("username")) + displayName := strings.TrimSpace(r.FormValue("display_name")) password := r.FormValue("password") role := strings.TrimSpace(r.FormValue("role")) active := r.FormValue("active") == "1" @@ -413,7 +414,7 @@ func userSaveHandler(w http.ResponseWriter, r *http.Request) { return } } - if err := SaveAppUser(username, hash, role, active); err != nil { + if err := SaveAppUser(username, displayName, hash, role, active); err != nil { redirectUsers(w, r, username, err.Error()) return } @@ -482,7 +483,7 @@ func userToggleActiveHandler(w http.ResponseWriter, r *http.Request) { redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins sperren.") return } - if err := SaveAppUser(username, "", target.Role, active); err != nil { + if err := SaveAppUser(username, target.DisplayName, "", target.Role, active); err != nil { redirectUsers(w, r, username, err.Error()) return } @@ -503,9 +504,9 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard - + - +
@@ -548,9 +549,9 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard - + - +
@@ -596,9 +597,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed fmt.Fprintf(w, ` Benutzerverwaltung - Mail-Graveyard - + - +
@@ -634,7 +635,7 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed fmt.Fprint(w, `
angemeldet als `) - fmt.Fprint(w, html.EscapeString(CurrentUser(r).Username+" / "+CurrentUser(r).Role)) + fmt.Fprint(w, html.EscapeString(userDisplayLabel(CurrentUser(r))+" / "+CurrentUser(r).Role)) fmt.Fprint(w, `
`) } @@ -666,9 +667,9 @@ func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Acc fmt.Fprintf(w, ` %s - Mail-Graveyard - + - +
@@ -707,9 +708,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, accounts []Account) { fmt.Fprintf(w, ` Quell-Postfach - Mail-Graveyard - + - +
@@ -749,9 +750,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, accounts []Account) { fmt.Fprintf(w, ` Ziel-Postfach - Mail-Graveyard - + - +
@@ -999,7 +1000,7 @@ func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) { fmt.Fprint(w, `
Noch keine Benutzer eingetragen.
`) return } - fmt.Fprint(w, ``) + fmt.Fprint(w, ``) for _, u := range users { activeClass := "" if u.Username == editName { @@ -1020,8 +1021,8 @@ func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) { toggleButton := fmt.Sprintf(``, html.EscapeString(u.Username), nextActive, toggleClass, toggleLabel) deleteButton := fmt.Sprintf(``, html.EscapeString(u.Username)) - fmt.Fprintf(w, ``, - activeClass, html.EscapeString(u.Username), html.EscapeString(u.Role), status, editButton, toggleButton, deleteButton) + fmt.Fprintf(w, ``, + activeClass, html.EscapeString(userDisplayName(u)), html.EscapeString(u.Username), html.EscapeString(u.Role), status, editButton, toggleButton, deleteButton) } fmt.Fprint(w, ``) } @@ -1033,11 +1034,12 @@ func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) { } fmt.Fprintf(w, ``, - html.EscapeString(u.Username), checked(u.Active), passwordHint(u.Username), passwordPlaceholder(u.Username), roleField) + html.EscapeString(u.Username), checked(u.Active), html.EscapeString(u.DisplayName), passwordHint(u.Username), passwordPlaceholder(u.Username), roleField) +} + +func userDisplayName(u AppUser) string { + name := strings.TrimSpace(u.DisplayName) + if name != "" { + return name + } + return u.Username +} + +func userDisplayLabel(u AppUser) string { + name := strings.TrimSpace(u.DisplayName) + if name != "" { + return name + " <" + u.Username + ">" + } + return u.Username } func roleSelect(current string, admin bool) string { diff --git a/backend/02-database.go b/backend/02-database.go index 6f47763..787701d 100644 --- a/backend/02-database.go +++ b/backend/02-database.go @@ -41,6 +41,7 @@ type Account struct { type AppUser struct { ID int64 Username string + DisplayName string Role string Active bool PasswordHash string @@ -119,6 +120,7 @@ func ConnectDB() error { `CREATE TABLE IF NOT EXISTS app_users( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', active INTEGER NOT NULL DEFAULT 1, @@ -139,6 +141,41 @@ func ConnectDB() error { } } DB = db + if err := ensureAppUserColumns(); err != nil { + _ = db.Close() + return err + } + return nil +} + +func ensureAppUserColumns() error { + hasDisplayName := false + rows, err := DB.Query(`PRAGMA table_info(app_users)`) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var cid int + var name, typ string + var notNull int + var defaultValue any + var pk int + if err := rows.Scan(&cid, &name, &typ, ¬Null, &defaultValue, &pk); err != nil { + return err + } + if name == "display_name" { + hasDisplayName = true + } + } + if err := rows.Err(); err != nil { + return err + } + if !hasDisplayName { + if _, err := DB.Exec(`ALTER TABLE app_users ADD COLUMN display_name TEXT NOT NULL DEFAULT ''`); err != nil { + return err + } + } return nil } @@ -149,7 +186,7 @@ func CountAppUsers() (int, error) { } func ListAppUsers() ([]AppUser, error) { - rows, err := DB.Query(`SELECT id, username, password_hash, role, active FROM app_users ORDER BY username`) + rows, err := DB.Query(`SELECT id, username, display_name, password_hash, role, active FROM app_users ORDER BY username`) if err != nil { return nil, err } @@ -166,30 +203,32 @@ func ListAppUsers() ([]AppUser, error) { } func GetAppUser(username string) (AppUser, error) { - row := DB.QueryRow(`SELECT id, username, password_hash, role, active FROM app_users WHERE username=?`, username) + row := DB.QueryRow(`SELECT id, username, display_name, password_hash, role, active FROM app_users WHERE username=?`, username) return scanAppUser(row) } func GetAppUserByID(id int64) (AppUser, error) { - row := DB.QueryRow(`SELECT id, username, password_hash, role, active FROM app_users WHERE id=?`, id) + row := DB.QueryRow(`SELECT id, username, display_name, password_hash, role, active FROM app_users WHERE id=?`, id) return scanAppUser(row) } -func SaveAppUser(username, passwordHash, role string, active bool) error { +func SaveAppUser(username, displayName, passwordHash, role string, active bool) error { + displayName = strings.TrimSpace(displayName) role = normalizeRole(role) if passwordHash == "" { - _, err := DB.Exec(`UPDATE app_users SET role=?, active=?, updated_at=CURRENT_TIMESTAMP WHERE username=?`, - role, boolInt(active), username) + _, err := DB.Exec(`UPDATE app_users SET display_name=?, role=?, active=?, updated_at=CURRENT_TIMESTAMP WHERE username=?`, + displayName, role, boolInt(active), username) return err } - _, err := DB.Exec(`INSERT INTO app_users(username, password_hash, role, active) - VALUES(?,?,?,?) + _, err := DB.Exec(`INSERT INTO app_users(username, display_name, password_hash, role, active) + VALUES(?,?,?,?,?) ON CONFLICT(username) DO UPDATE SET + display_name=excluded.display_name, password_hash=excluded.password_hash, role=excluded.role, active=excluded.active, updated_at=CURRENT_TIMESTAMP`, - username, passwordHash, role, boolInt(active)) + username, displayName, passwordHash, role, boolInt(active)) return err } @@ -209,7 +248,7 @@ func DeleteSession(token string) error { } func SessionUser(token string) (AppUser, error) { - row := DB.QueryRow(`SELECT u.id, u.username, u.password_hash, u.role, u.active + row := DB.QueryRow(`SELECT u.id, u.username, u.display_name, u.password_hash, u.role, u.active FROM app_sessions s JOIN app_users u ON u.id=s.user_id WHERE s.token=? AND s.expires_at > CURRENT_TIMESTAMP AND u.active=1`, token) return scanAppUser(row) @@ -351,7 +390,7 @@ func normalizeAccount(a *Account) { func scanAppUser(s appUserScanner) (AppUser, error) { var u AppUser var active int - err := s.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &active) + err := s.Scan(&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &u.Role, &active) u.Role = normalizeRole(u.Role) u.Active = active != 0 return u, err diff --git a/backend/03-auth.go b/backend/03-auth.go index 6d3755f..102a93e 100644 --- a/backend/03-auth.go +++ b/backend/03-auth.go @@ -50,7 +50,7 @@ func InitAuth() error { if err != nil { return err } - return SaveAppUser(strings.TrimSpace(Cfg.AdminUser), hash, roleAdmin, true) + return SaveAppUser(strings.TrimSpace(Cfg.AdminUser), "Admin", hash, roleAdmin, true) } // AuthMiddleware schuetzt alle Routen ausser /login und /static. @@ -169,7 +169,7 @@ func renderLoginPage(w http.ResponseWriter, errMsg string) { fmt.Fprintf(w, ` Login - Mail-Graveyard - +