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) {
+
+}
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, "")
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, "")
+ 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)
+}