User Account Management page with search for users
This commit is contained in:
+1
-16
@@ -218,21 +218,6 @@ type CMData struct {
|
|||||||
Level uint16
|
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.
|
/* ConferenceMembers shows the conference members and allows their access levels to be adjusted.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
@@ -350,7 +335,7 @@ func ConferenceMembers(ctxt ui.AmContext) (string, any) {
|
|||||||
mr[i].Level = members[i].Level
|
mr[i].Level = members[i].Level
|
||||||
}
|
}
|
||||||
case "comm":
|
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 {
|
if err != nil {
|
||||||
return "error", err
|
return "error", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ import (
|
|||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"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.
|
// loadCategoryInformation loads the current category information to the context.
|
||||||
func loadCategoryInformation(ctxt ui.AmContext, offset int) error {
|
func loadCategoryInformation(ctxt ui.AmContext, offset int) error {
|
||||||
if ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
|
if ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
|
||||||
@@ -217,27 +232,12 @@ func Find(ctxt ui.AmContext) (string, any) {
|
|||||||
}
|
}
|
||||||
case "USR":
|
case "USR":
|
||||||
var iField, iOper int
|
var iField, iOper int
|
||||||
switch field {
|
var ok bool
|
||||||
case "name":
|
if iField, ok = SearchUserFieldMap[field]; !ok {
|
||||||
iField = database.SearchUserFieldName
|
|
||||||
case "descr":
|
|
||||||
iField = database.SearchUserFieldDescription
|
|
||||||
case "first":
|
|
||||||
iField = database.SearchUserFieldFirstName
|
|
||||||
case "last":
|
|
||||||
iField = database.SearchUserFieldLastName
|
|
||||||
default:
|
|
||||||
ctxt.VarMap().Set("errorMessage", "invalid parameter to find")
|
ctxt.VarMap().Set("errorMessage", "invalid parameter to find")
|
||||||
return "framed", "find.jet"
|
return "framed", "find.jet"
|
||||||
}
|
}
|
||||||
switch oper {
|
if iOper, ok = SearchUserOperMap[oper]; !ok {
|
||||||
case "st":
|
|
||||||
iOper = database.SearchUserOperPrefix
|
|
||||||
case "in":
|
|
||||||
iOper = database.SearchUserOperSubstring
|
|
||||||
case "re":
|
|
||||||
iOper = database.SearchUserOperRegex
|
|
||||||
default:
|
|
||||||
ctxt.VarMap().Set("errorMessage", "invalid parameter to find")
|
ctxt.VarMap().Set("errorMessage", "invalid parameter to find")
|
||||||
return "framed", "find.jet"
|
return "framed", "find.jet"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var GetAndPost = []string{http.MethodGet, http.MethodPost}
|
||||||
|
|
||||||
// setupEcho creates, configures, and returns a new Echo instance.
|
// setupEcho creates, configures, and returns a new Echo instance.
|
||||||
func setupEcho() *echo.Echo {
|
func setupEcho() *echo.Echo {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
@@ -46,9 +48,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.Use(LogrusMiddleware, ui.SessionStoreInjector, ui.ContextCreator)
|
e.Use(LogrusMiddleware, ui.SessionStoreInjector, ui.ContextCreator)
|
||||||
e.Use(ui.IPBanTest, ui.CookieLoginTest)
|
e.Use(ui.IPBanTest, ui.CookieLoginTest)
|
||||||
|
|
||||||
fn := ui.AmWrap(NotImplPage)
|
e.Match(GetAndPost, "/TODO/*", ui.AmWrap(NotImplPage))
|
||||||
e.GET("/TODO/*", fn)
|
|
||||||
e.POST("/TODO/*", fn)
|
|
||||||
e.GET("/img/*", ui.AmServeImage)
|
e.GET("/img/*", ui.AmServeImage)
|
||||||
e.GET("/static/*", ui.AmStaticFileHandler())
|
e.GET("/static/*", ui.AmStaticFileHandler())
|
||||||
e.GET("/go/:postlink", ui.AmWrap(JumpToShortcut))
|
e.GET("/go/:postlink", ui.AmWrap(JumpToShortcut))
|
||||||
@@ -78,6 +78,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
||||||
e.GET("/sysadmin/globals", ui.AmWrap(GlobalPropertiesForm))
|
e.GET("/sysadmin/globals", ui.AmWrap(GlobalPropertiesForm))
|
||||||
e.POST("/sysadmin/globals", ui.AmWrap(GlobalPropertiesSet))
|
e.POST("/sysadmin/globals", ui.AmWrap(GlobalPropertiesSet))
|
||||||
|
e.Match(GetAndPost, "/sysadmin/users", ui.AmWrap(UserManagementSearch))
|
||||||
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||||
e.GET("/manage_comm", ui.AmWrap(ManageCommunities))
|
e.GET("/manage_comm", ui.AmWrap(ManageCommunities))
|
||||||
@@ -87,7 +88,7 @@ func setupEcho() *echo.Echo {
|
|||||||
|
|
||||||
// community group
|
// community group
|
||||||
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
||||||
fn = ui.AmWrap(ShowCommunity)
|
fn := ui.AmWrap(ShowCommunity)
|
||||||
commGroup.GET("", fn)
|
commGroup.GET("", fn)
|
||||||
commGroup.GET("/profile", fn)
|
commGroup.GET("/profile", fn)
|
||||||
commGroup.GET("/join", ui.AmWrap(JoinCommunity))
|
commGroup.GET("/join", ui.AmWrap(JoinCommunity))
|
||||||
@@ -123,9 +124,7 @@ func setupEcho() *echo.Echo {
|
|||||||
confGroup.POST("/edit", ui.AmWrap(EditConference))
|
confGroup.POST("/edit", ui.AmWrap(EditConference))
|
||||||
confGroup.GET("/aliases", ui.AmWrap(ConferenceAliasForm))
|
confGroup.GET("/aliases", ui.AmWrap(ConferenceAliasForm))
|
||||||
confGroup.POST("/aliases", ui.AmWrap(ConferenceAliasAdd))
|
confGroup.POST("/aliases", ui.AmWrap(ConferenceAliasAdd))
|
||||||
fn = ui.AmWrap(ConferenceMembers)
|
confGroup.Match(GetAndPost, "/members", ui.AmWrap(ConferenceMembers))
|
||||||
confGroup.GET("/members", fn)
|
|
||||||
confGroup.POST("/members", fn)
|
|
||||||
confGroup.GET("/custom", ui.AmWrap(ConfCustomForm))
|
confGroup.GET("/custom", ui.AmWrap(ConfCustomForm))
|
||||||
confGroup.POST("/custom", ui.AmWrap(ConfCustom))
|
confGroup.POST("/custom", ui.AmWrap(ConfCustom))
|
||||||
confGroup.GET("/activity", ui.AmWrap(ConfReports))
|
confGroup.GET("/activity", ui.AmWrap(ConfReports))
|
||||||
|
|||||||
+67
@@ -11,6 +11,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
)
|
)
|
||||||
@@ -138,3 +141,67 @@ func GlobalPropertiesSet(ctxt ui.AmContext) (string, any) {
|
|||||||
}
|
}
|
||||||
return "redirect", "/sysadmin"
|
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"
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ menudefs:
|
|||||||
disabled: true
|
disabled: true
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- text: "User Account Management"
|
- text: "User Account Management"
|
||||||
link: "/TODO/sysadmin/find_user"
|
link: "/sysadmin/users"
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- text: "System Audit Logs"
|
- text: "System Audit Logs"
|
||||||
link: "/TODO/sysadmin/audit"
|
link: "/TODO/sysadmin/audit"
|
||||||
|
|||||||
@@ -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/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold mb-2">User Account Management</h1>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backlink -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="/sysadmin">
|
||||||
|
<span>←</span>
|
||||||
|
Return to System Administration Menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(errorMessage) }}
|
||||||
|
<!-- Error Message Banner -->
|
||||||
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6" id="error-banner">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-red-500 text-xl">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm font-medium" id="error-message">{{ CapitalizeString(errorMessage) }}.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Search Form -->
|
||||||
|
<div class="max-w-3xl mb-8">
|
||||||
|
<form method="POST" action="/sysadmin/users">
|
||||||
|
<input type="hidden" name="ofs" value="0">
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<h2 class="text-xl font-bold text-black mb-4">Find Users:</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Field Selection -->
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-black">Display all users whose</span>
|
||||||
|
<select name="field"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="name" {{ if field == "name" }}selected{{ end }}>user name</option>
|
||||||
|
<option value="descr"{{ if field == "descr" }}selected{{ end }}>description</option>
|
||||||
|
<option value="first"{{ if field == "first" }}selected{{ end }}>first name</option>
|
||||||
|
<option value="last"{{ if field == "last" }}selected{{ end }}>last name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Criteria -->
|
||||||
|
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||||
|
<select name="oper"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="st" {{ if oper == "st" }}selected{{ end }}>starts with the string</option>
|
||||||
|
<option value="in" {{ if oper == "in" }}selected{{ end }}>contains the string</option>
|
||||||
|
<option value="re" {{ if oper == "in" }}selected{{ end }}>matches the regular expression</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="term" size="32" maxlength="255" value="{{ term }}"
|
||||||
|
placeholder="Enter search term..."
|
||||||
|
class="flex-1 min-w-64 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Button -->
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="search"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(resultHeader) }}
|
||||||
|
<!-- Search results -->
|
||||||
|
<hr class="border-gray-400 mb-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<div class="text-sm text-black"><b>Search Results</b> ({{ resultHeader }})</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(resultList) }}
|
||||||
|
<!-- Results List -->
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<div class="space-y-4">
|
||||||
|
{{ range _, rx := resultList }}
|
||||||
|
<!-- User Result -->
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="text-sm pt-0.5 flex-shrink-0">🟣</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-2">
|
||||||
|
<a href="/user/{{ rx.Username }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900 font-bold text-base">{{ rx.Username }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-700 space-y-1">
|
||||||
|
<div>
|
||||||
|
{{ ci := UserContactInfo(rx, .) }}
|
||||||
|
{{ DisplayFullName(ci) }}, from {{ ci.Locality }}, {{ ci.Region }} {{ ci.Country }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm space-y-1">
|
||||||
|
<a href="/sysadmin/users/{{ rx.Username }}" class="text-blue-700 hover:text-blue-900 font-bold text-base">[Modify User]</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ if isset(resultShowPrev) || isset(resultShowNext) }}
|
||||||
|
<div class="flex justify-center gap-4 mt-6">
|
||||||
|
<form method="POST" action="/sysadmin/users">
|
||||||
|
<input type="hidden" name="ofs" value="{{ ofs }}"/>
|
||||||
|
<input type="hidden" name="field" value="{{ field }}"/>
|
||||||
|
<input type="hidden" name="oper" value="{{ oper }}"/>
|
||||||
|
<input type="hidden" name="term" value="{{ term }}"/>
|
||||||
|
{{ if isset(resultShowPrev) }}
|
||||||
|
<button type="submit" name="prev"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
⏪ Prev
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(resultShowNext) }}
|
||||||
|
<button type="submit" name="next"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
Next ⏩
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user