From 5070eb0a7936af734a7be5b85a30ff234150230d Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Mon, 16 Feb 2026 22:44:59 -0700 Subject: [PATCH] User Account Management page with search for users --- conferenceadmin.go | 17 +---- find.go | 36 +++++----- main.go | 13 ++-- sysadmin.go | 67 +++++++++++++++++++ ui/menudefs.yaml | 2 +- ui/views/admin_users.jet | 140 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+), 42 deletions(-) create mode 100644 ui/views/admin_users.jet diff --git a/conferenceadmin.go b/conferenceadmin.go index 132f54e..cdf6e87 100644 --- a/conferenceadmin.go +++ b/conferenceadmin.go @@ -218,21 +218,6 @@ type CMData struct { Level uint16 } -// fieldMap maps field names to search field indexes. -var fieldMap = map[string]int{ - "name": database.SearchUserFieldName, - "descr": database.SearchUserFieldDescription, - "first": database.SearchUserFieldFirstName, - "last": database.SearchUserFieldLastName, -} - -// operMap maps operator names to search operator indices. -var operMap = map[string]int{ - "st": database.SearchUserOperPrefix, - "in": database.SearchUserOperSubstring, - "re": database.SearchUserOperRegex, -} - /* ConferenceMembers shows the conference members and allows their access levels to be adjusted. * Parameters: * ctxt - The AmContext for the request. @@ -350,7 +335,7 @@ func ConferenceMembers(ctxt ui.AmContext) (string, any) { mr[i].Level = members[i].Level } case "comm": - ulist, t, err := database.AmSearchCommunityMembers(ctxt.Ctx(), comm, fieldMap[field], operMap[oper], term, offset, int(maxPage)) + ulist, t, err := database.AmSearchCommunityMembers(ctxt.Ctx(), comm, SearchUserFieldMap[field], SearchUserOperMap[oper], term, offset, int(maxPage)) if err != nil { return "error", err } diff --git a/find.go b/find.go index 8481ae9..8f6a7ee 100644 --- a/find.go +++ b/find.go @@ -17,6 +17,21 @@ import ( "git.erbosoft.com/amy/amsterdam/ui" ) +// SearchUserFieldMap maps field names to search field indexes. +var SearchUserFieldMap = map[string]int{ + "name": database.SearchUserFieldName, + "descr": database.SearchUserFieldDescription, + "first": database.SearchUserFieldFirstName, + "last": database.SearchUserFieldLastName, +} + +// SearchUserOperMap maps operator names to search operator indices. +var SearchUserOperMap = map[string]int{ + "st": database.SearchUserOperPrefix, + "in": database.SearchUserOperSubstring, + "re": database.SearchUserOperRegex, +} + // loadCategoryInformation loads the current category information to the context. func loadCategoryInformation(ctxt ui.AmContext, offset int) error { if ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { @@ -217,27 +232,12 @@ func Find(ctxt ui.AmContext) (string, any) { } case "USR": var iField, iOper int - switch field { - case "name": - iField = database.SearchUserFieldName - case "descr": - iField = database.SearchUserFieldDescription - case "first": - iField = database.SearchUserFieldFirstName - case "last": - iField = database.SearchUserFieldLastName - default: + var ok bool + if iField, ok = SearchUserFieldMap[field]; !ok { ctxt.VarMap().Set("errorMessage", "invalid parameter to find") return "framed", "find.jet" } - switch oper { - case "st": - iOper = database.SearchUserOperPrefix - case "in": - iOper = database.SearchUserOperSubstring - case "re": - iOper = database.SearchUserOperRegex - default: + if iOper, ok = SearchUserOperMap[oper]; !ok { ctxt.VarMap().Set("errorMessage", "invalid parameter to find") return "framed", "find.jet" } diff --git a/main.go b/main.go index 526716c..6c321d5 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,8 @@ import ( log "github.com/sirupsen/logrus" ) +var GetAndPost = []string{http.MethodGet, http.MethodPost} + // setupEcho creates, configures, and returns a new Echo instance. func setupEcho() *echo.Echo { e := echo.New() @@ -46,9 +48,7 @@ func setupEcho() *echo.Echo { e.Use(LogrusMiddleware, ui.SessionStoreInjector, ui.ContextCreator) e.Use(ui.IPBanTest, ui.CookieLoginTest) - fn := ui.AmWrap(NotImplPage) - e.GET("/TODO/*", fn) - e.POST("/TODO/*", fn) + e.Match(GetAndPost, "/TODO/*", ui.AmWrap(NotImplPage)) e.GET("/img/*", ui.AmServeImage) e.GET("/static/*", ui.AmStaticFileHandler()) e.GET("/go/:postlink", ui.AmWrap(JumpToShortcut)) @@ -78,6 +78,7 @@ func setupEcho() *echo.Echo { e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) e.GET("/sysadmin/globals", ui.AmWrap(GlobalPropertiesForm)) e.POST("/sysadmin/globals", ui.AmWrap(GlobalPropertiesSet)) + e.Match(GetAndPost, "/sysadmin/users", ui.AmWrap(UserManagementSearch)) e.GET("/create_comm", ui.AmWrap(CreateCommunityForm)) e.POST("/create_comm", ui.AmWrap(CreateCommunity)) e.GET("/manage_comm", ui.AmWrap(ManageCommunities)) @@ -87,7 +88,7 @@ func setupEcho() *echo.Echo { // community group commGroup := e.Group("/comm/:cid", ui.SetCommunity) - fn = ui.AmWrap(ShowCommunity) + fn := ui.AmWrap(ShowCommunity) commGroup.GET("", fn) commGroup.GET("/profile", fn) commGroup.GET("/join", ui.AmWrap(JoinCommunity)) @@ -123,9 +124,7 @@ func setupEcho() *echo.Echo { confGroup.POST("/edit", ui.AmWrap(EditConference)) confGroup.GET("/aliases", ui.AmWrap(ConferenceAliasForm)) confGroup.POST("/aliases", ui.AmWrap(ConferenceAliasAdd)) - fn = ui.AmWrap(ConferenceMembers) - confGroup.GET("/members", fn) - confGroup.POST("/members", fn) + confGroup.Match(GetAndPost, "/members", ui.AmWrap(ConferenceMembers)) confGroup.GET("/custom", ui.AmWrap(ConfCustomForm)) confGroup.POST("/custom", ui.AmWrap(ConfCustom)) confGroup.GET("/activity", ui.AmWrap(ConfReports)) diff --git a/sysadmin.go b/sysadmin.go index d855212..d1c99d1 100644 --- a/sysadmin.go +++ b/sysadmin.go @@ -11,6 +11,9 @@ package main import ( + "fmt" + "strconv" + "git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/ui" ) @@ -138,3 +141,67 @@ func GlobalPropertiesSet(ctxt ui.AmContext) (string, any) { } return "redirect", "/sysadmin" } + +/* UserManagementSearch displays the user management page and performs searches. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + */ +func UserManagementSearch(ctxt ui.AmContext) (string, any) { + if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) { + return "error", ENOACCESS + } + + field := "name" + oper := "st" + term := "" + ofs := 0 + doSearch := false + listMax := int(ctxt.Globals().MaxSearchPage) + if ctxt.Verb() == "POST" { + field = ctxt.FormField("field") + oper = ctxt.FormField("oper") + term = ctxt.FormField("term") + ofsStr := ctxt.FormField("ofs") + if n, err := strconv.Atoi(ofsStr); err == nil { + ofs = n + } + if ctxt.FormFieldIsSet("prev") { + ofs = max(0, ofs-listMax) + } else if ctxt.FormFieldIsSet("next") { + ofs += listMax + } + doSearch = true + } + ctxt.VarMap().Set("field", field) + ctxt.VarMap().Set("oper", oper) + ctxt.VarMap().Set("term", term) + ctxt.VarMap().Set("ofs", ofs) + if doSearch { + ulist, total, err := database.AmSearchUsers(ctxt.Ctx(), SearchUserFieldMap[field], SearchUserOperMap[oper], term, ofs, listMax) + if err == nil { + resultLine := "" + if len(ulist) == 0 { + resultLine = "None found" + } else { + resultLine = fmt.Sprintf("Displaying %d-%d of %d", ofs+1, ofs+len(ulist), total) + } + ctxt.VarMap().Set("resultHeader", resultLine) + if len(ulist) > 0 { + ctxt.VarMap().Set("resultList", ulist) + if ofs > 0 { + ctxt.VarMap().Set("resultShowPrev", true) + } + if (ofs + listMax) < total { + ctxt.VarMap().Set("resultShowNext", true) + } + } + } else { + ctxt.VarMap().Set("errorMessage", err.Error()) + } + } + ctxt.SetFrameTitle("User Account Management") + return "framed", "admin_users.jet" +} diff --git a/ui/menudefs.yaml b/ui/menudefs.yaml index e01135c..11968ad 100644 --- a/ui/menudefs.yaml +++ b/ui/menudefs.yaml @@ -44,7 +44,7 @@ menudefs: disabled: true permission: "Global.SysAdminAccess" - text: "User Account Management" - link: "/TODO/sysadmin/find_user" + link: "/sysadmin/users" permission: "Global.SysAdminAccess" - text: "System Audit Logs" link: "/TODO/sysadmin/audit" diff --git a/ui/views/admin_users.jet b/ui/views/admin_users.jet new file mode 100644 index 0000000..8531c1d --- /dev/null +++ b/ui/views/admin_users.jet @@ -0,0 +1,140 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + *} +
+ +
+

User Account Management

+
+
+ + + + + {{ if isset(errorMessage) }} + +
+
+
+ ⚠️ +
+
+

{{ CapitalizeString(errorMessage) }}.

+
+
+
+ {{ end }} + + +
+
+ +
+

Find Users:

+
+ +
+ Display all users whose + +
+ + +
+ + +
+ + +
+ +
+
+
+
+
+ + {{ if isset(resultHeader) }} + +
+
+
Search Results ({{ resultHeader }})
+
+ + {{ if isset(resultList) }} + +
+
+ {{ range _, rx := resultList }} + +
+ 🟣 +
+ +
+
+ {{ ci := UserContactInfo(rx, .) }} + {{ DisplayFullName(ci) }}, from {{ ci.Locality }}, {{ ci.Region }} {{ ci.Country }} +
+
+ +
+
+ {{ end }} +
+ {{ if isset(resultShowPrev) || isset(resultShowNext) }} +
+
+ + + + + {{ if isset(resultShowPrev) }} + + {{ end }} + {{ if isset(resultShowNext) }} + + {{ end }} +
+
+ {{ end }} +
+ {{ end }} + {{ end }} +