From 19f1cf2c2bafe1d5c816db00b5b40d267d4a9162 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Mon, 15 Jun 2026 06:04:58 +0000 Subject: [PATCH] feat(users): edit form with role/group edit, delete on edit page --- src/core/admin/templates/layout.templ | 2 + src/core/admin/templates/layout_templ.go | 2 +- src/core/admin/templates/user-dashboard.templ | 85 ++++- .../admin/templates/user-dashboard_templ.go | 334 ++++++++++++++---- src/core/admin/ui.go | 34 ++ 5 files changed, 381 insertions(+), 76 deletions(-) diff --git a/src/core/admin/templates/layout.templ b/src/core/admin/templates/layout.templ index 8455248..a741f6d 100644 --- a/src/core/admin/templates/layout.templ +++ b/src/core/admin/templates/layout.templ @@ -100,6 +100,8 @@ templ adminStyles() { .btn-sm:hover{background:var(--surface2)} .btn-sm.danger{color:var(--red);border-color:var(--red)} .btn-sm.danger:hover{background:rgba(248,81,73,0.1)} + .btn.danger{background:var(--red)} + .btn.danger:hover{opacity:0.9} .table-toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .table-title{font-weight:600;font-size:0.9rem} .user-table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px} diff --git a/src/core/admin/templates/layout_templ.go b/src/core/admin/templates/layout_templ.go index 88887c7..4b516ce 100644 --- a/src/core/admin/templates/layout_templ.go +++ b/src/core/admin/templates/layout_templ.go @@ -169,7 +169,7 @@ func adminStyles() templ.Component { templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/src/core/admin/templates/user-dashboard.templ b/src/core/admin/templates/user-dashboard.templ index 53e6b8e..d8f4017 100644 --- a/src/core/admin/templates/user-dashboard.templ +++ b/src/core/admin/templates/user-dashboard.templ @@ -48,10 +48,7 @@ templ UserList(users []UserRow) { } else { } - + } @@ -124,6 +121,35 @@ func initials(s string) string { return string(s[0]) } +func hasGroup(userGroups, group string) bool { + for _, g := range splitGroups(userGroups) { + if g == group { + return true + } + } + return false +} + +func splitGroups(s string) []string { + if s == "" { return nil } + var result []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == ',' { + result = append(result, trimSpace(s[start:i])) + start = i + 1 + } + } + result = append(result, trimSpace(s[start:])) + return result +} + +func trimSpace(s string) string { + for len(s) > 0 && s[0] == ' ' { s = s[1:] } + for len(s) > 0 && s[len(s)-1] == ' ' { s = s[:len(s)-1] } + return s +} + type UserRow struct { Username string DisplayName string @@ -138,3 +164,54 @@ type CreateUserResultRow struct { GeneratedPassword string Error string } + +templ EditUserForm(u UserRow, groups []string) { +
+
+

Edit User

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ if len(groups) > 0 { +
+ +
+ for _, g := range groups { + + } +
+
+ } +
+ + +
+
+
+} diff --git a/src/core/admin/templates/user-dashboard_templ.go b/src/core/admin/templates/user-dashboard_templ.go index 02e92e3..41f47f7 100644 --- a/src/core/admin/templates/user-dashboard_templ.go +++ b/src/core/admin/templates/user-dashboard_templ.go @@ -150,33 +150,20 @@ func UserList(users []UserRow) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"#user-detail\" hx-swap=\"innerHTML\">Edit") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -202,58 +189,58 @@ func CreateUserForm(groups []string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - templ_7745c5c3_Var10 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

New User

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

New User

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(groups) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, g := range groups { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -277,64 +264,64 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) for _, r := range results { if r.Error != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 105, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error) + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 108, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 108, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " created
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username) + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 111, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 109, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " created
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 112, Col: 51} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Save this password — it won't be shown again
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
Save this password — it won't be shown again
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -349,6 +336,41 @@ func initials(s string) string { return string(s[0]) } +func hasGroup(userGroups, group string) bool { + for _, g := range splitGroups(userGroups) { + if g == group { + return true + } + } + return false +} + +func splitGroups(s string) []string { + if s == "" { + return nil + } + var result []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == ',' { + result = append(result, trimSpace(s[start:i])) + start = i + 1 + } + } + result = append(result, trimSpace(s[start:])) + return result +} + +func trimSpace(s string) string { + for len(s) > 0 && s[0] == ' ' { + s = s[1:] + } + for len(s) > 0 && s[len(s)-1] == ' ' { + s = s[:len(s)-1] + } + return s +} + type UserRow struct { Username string DisplayName string @@ -364,4 +386,174 @@ type CreateUserResultRow struct { Error string } +func EditUserForm(u UserRow, groups []string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

Edit User

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(groups) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, g := range groups { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + var _ = templruntime.GeneratedTemplate diff --git a/src/core/admin/ui.go b/src/core/admin/ui.go index 9babcde..9340d3e 100644 --- a/src/core/admin/ui.go +++ b/src/core/admin/ui.go @@ -19,6 +19,8 @@ func (h *Handler) RegisterUIRoutes(mux *http.ServeMux, authMiddleware func(http. mux.Handle("GET /admin/users", authMiddleware(http.HandlerFunc(h.adminUsers))) mux.Handle("GET /admin/users/create-form", authMiddleware(http.HandlerFunc(h.createUserForm))) mux.Handle("GET /admin/users/cancel-form", authMiddleware(http.HandlerFunc(h.cancelForm))) + mux.Handle("GET /admin/users/edit-form/{username}", authMiddleware(http.HandlerFunc(h.editUserForm))) + mux.Handle("PUT /admin/users/{username}", authMiddleware(http.HandlerFunc(h.updateUser))) // Groups mux.Handle("GET /admin/groups", authMiddleware(http.HandlerFunc(h.groupsPage))) mux.Handle("GET /admin/groups/list", authMiddleware(http.HandlerFunc(h.groupList))) @@ -396,3 +398,35 @@ func (h *Handler) updateGroup(w http.ResponseWriter, r *http.Request) { } h.groupList(w, r) } + +func (h *Handler) editUserForm(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("username") + u, _ := h.store.GetByUsername(username) + if u == nil { + http.Error(w, "user not found", http.StatusNotFound) + return + } + grps, _ := h.groupStore.List() + groupNames := make([]string, 0, len(grps)) + for _, g := range grps { + groupNames = append(groupNames, g.Name) + } + row := UserToRow(*u) + component := templates.EditUserForm(row, groupNames) + component.Render(r.Context(), w) +} + +func (h *Handler) updateUser(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("username") + r.ParseForm() + role := r.FormValue("role") + groupVals := r.Form["group"] + groups := "" + for i, g := range groupVals { + if i > 0 { groups += "," } + groups += g + } + h.store.GetDB().Exec(`UPDATE users SET role = ?, groups = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, role, groups, username) + h.syncWriter.Sync() + h.userRowsHandler(w, r) +}