diff --git a/communityadmin.go b/communityadmin.go index 9a86dca..9a7fc62 100644 --- a/communityadmin.go +++ b/communityadmin.go @@ -481,6 +481,139 @@ func CommunityCategory(ctxt ui.AmContext) (string, any) { return "framed", "comm_category.jet" } +func CommunityMembers(ctxt ui.AmContext) (string, any) { + comm := ctxt.CurrentCommunity() + if !comm.TestPermission("Community.Write", ctxt.EffectiveLevel()) { + return "error", ENOACCESS + } + + // Set the first batch of page variables. + hostRole := database.AmRole("Community.Host") + ctxt.VarMap().Set("commName", comm.Name) + ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/admin", comm.Alias)) + ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/admin/members", comm.Alias)) + ctxt.VarMap().Set("roleList", database.AmRoleList("Community.Userlevels")) + ctxt.VarMap().Set("hostRole", hostRole) + ctxt.SetFrameTitle(fmt.Sprintf("Membership in Community: %s", comm.Name)) + + // Get the search parameter values and adjust them. + mode := "comm" + field := "name" + oper := "st" + term := "" + offset := 0 + if ctxt.Verb() == "POST" { + mode = ctxt.FormField("mode") + field = ctxt.FormField("field") + oper = ctxt.FormField("oper") + term = ctxt.FormField("term") + var e1 error + offset, e1 = ctxt.FormFieldInt("ofs") + if e1 != nil { + offset = 0 + } + } + maxPage := ctxt.Globals().MaxSearchPage + + // Adjust the offset based on the page buttons. + if ctxt.FormFieldIsSet("prev") { + offset = max(0, offset-int(maxPage)) + } else if ctxt.FormFieldIsSet("next") { + offset += int(maxPage) + } + + // Write the search parameters back to the page variables. + ctxt.VarMap().Set("mode", mode) + ctxt.VarMap().Set("field", field) + ctxt.VarMap().Set("oper", oper) + ctxt.VarMap().Set("term", term) + ctxt.VarMap().Set("offset", offset) + ctxt.VarMap().Set("max", maxPage) + + if ctxt.FormFieldIsSet("update") { + // Parse out the list of valid UIDs. + uids := util.Map(strings.Split(ctxt.FormField("validUids"), "|"), func(in string) int32 { + rc, err := strconv.Atoi(in) + if err != nil { + return -1 + } + return int32(rc) + }) + for _, uid := range uids { + if uid > 0 { + // Get old and new access levels from the form. + tmp, err := ctxt.FormFieldInt(fmt.Sprintf("old_%d", uid)) + if err == nil { + oldLevel := uint16(tmp) + if oldLevel == hostRole.Level() { + tmp = int(oldLevel) + } else { + tmp, err = ctxt.FormFieldInt(fmt.Sprintf("new_%d", uid)) + } + if err == nil { + newLevel := uint16(tmp) + oldLock := ctxt.FormField(fmt.Sprintf("oldlock_%d", uid)) == "1" + newLock := ctxt.FormField(fmt.Sprintf("lock_%d", uid)) == "Y" + if (oldLevel != newLevel) || (oldLock != newLock) { + // Update the level for this user. + var u *database.User + u, err = database.AmGetUser(ctxt.Ctx(), uid) + if err == nil { + err = comm.SetMembership(ctxt.Ctx(), u, newLevel, newLock, ctxt.CurrentUserId(), ctxt.RemoteIP()) + } + } + } + } + if err != nil { + return "error", err + } + } + } + ctxt.VarMap().Set("updated", true) + } + + // Generate the result list. + total := 0 + var err error + var userlist []*database.User + switch mode { + case "comm": + userlist, total, err = comm.ListMembers(ctxt.Ctx(), database.ListMembersFieldNone, database.ListMembersOperNone, "", offset, int(maxPage), false) + case "user": + userlist, total, err = database.AmSearchUsers(ctxt.Ctx(), SearchUserFieldMap[field], SearchUserOperMap[oper], term, offset, int(maxPage)) + } + if err != nil { + return "error", err + } + mr := make([]CMData, len(userlist)) + for i := range userlist { + mr[i].User = userlist[i] + var mbr bool + mbr, mr[i].Lock, mr[i].Level, err = comm.Membership(ctxt.Ctx(), userlist[i]) + if err != nil { + return "error", err + } + if !mbr { + mr[i].Level = 0 + mr[i].Lock = false + } + } + + // Set the last few variables and return. + ctxt.VarMap().Set("resultList", mr) + ctxt.VarMap().Set("total", total) + ctxt.VarMap().Set("validUids", strings.Join(util.Map(mr, func(cd CMData) string { + return fmt.Sprintf("%d", cd.User.Uid) + }), "|")) + if offset > 0 { + ctxt.VarMap().Set("showPrev", true) + } + if (offset + len(mr)) < total { + ctxt.VarMap().Set("showNext", true) + } + return "framed", "comm_members.jet" +} + /* CommunityEmailForm displays the form for sending mass mail to the community. * Parameters: * ctxt - The AmContext for the request. @@ -503,6 +636,14 @@ func CommunityEmailForm(ctxt ui.AmContext) (string, any) { return "framed", "comm_email.jet" } +/* CommunityEmail sends mass mail to the community. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ func CommunityEmail(ctxt ui.AmContext) (string, any) { comm := ctxt.CurrentCommunity() if !comm.TestPermission("Community.MassMail", ctxt.EffectiveLevel()) { diff --git a/conferenceadmin.go b/conferenceadmin.go index 41795ce..df05abb 100644 --- a/conferenceadmin.go +++ b/conferenceadmin.go @@ -214,10 +214,11 @@ func ConferenceAliasAdd(ctxt ui.AmContext) (string, any) { return "framed", "conf_aliases.jet" } -// CMData is the result data passed to the conference members page. +// CMData is the result data passed to the community or conference members page. type CMData struct { User *database.User Level uint16 + Lock bool } /* ConferenceMembers shows the conference members and allows their access levels to be adjusted. diff --git a/database/security.go b/database/security.go index 34ac39e..2856675 100644 --- a/database/security.go +++ b/database/security.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * 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 @@ -15,6 +15,7 @@ import ( "strconv" "strings" + "github.com/labstack/gommon/log" "gopkg.in/yaml.v3" ) @@ -239,7 +240,11 @@ func (r *CfgRoleList) FindForLevel(level uint16) Role { * The specified role. */ func AmRole(id string) Role { - return securityRoot.roleMap[id] + rc, ok := securityRoot.roleMap[id] + if !ok { + log.Errorf("AmRole('%s') - role not found!", id) + } + return rc } /* AmDefaultRole returns a Role given a default ID. @@ -249,7 +254,12 @@ func AmRole(id string) Role { * The specified role. */ func AmDefaultRole(id string) Role { - return securityRoot.defaultsMap[id].roleptr + dr, ok := securityRoot.defaultsMap[id] + if !ok { + log.Errorf("AmDefaultRole('%s') - default role not found!", id) + return nil + } + return dr.roleptr } /* AmRoleList returns a RoleList given a list ID. @@ -259,7 +269,11 @@ func AmDefaultRole(id string) Role { * The specified role list. */ func AmRoleList(id string) RoleList { - return securityRoot.listsMap[id] + rc, ok := securityRoot.listsMap[id] + if !ok { + log.Errorf("AmRoleList('%s') - role list not found!", id) + } + return rc } /* AmTestPermission tests a specified access level to see if it satisfies the given permission. @@ -270,12 +284,22 @@ func AmRoleList(id string) RoleList { * true if the permission test is satisfied, false if not. */ func AmTestPermission(id string, level uint16) bool { - return securityRoot.permsMap[id].level <= level + perm, ok := securityRoot.permsMap[id] + if !ok { + log.Errorf("AmTestPermission('%s') - permission not found!", id) + return false + } + return perm.level <= level } // AmPermissionLevel returns a level value for a permission. func AmPermissionLevel(id string) uint16 { - return securityRoot.permsMap[id].level + perm, ok := securityRoot.permsMap[id] + if !ok { + log.Errorf("AmPermissionLevel('%s') - permission not found!", id) + return 0 + } + return perm.level } /* AmCombinePermissionRole combines a permission and a role into a single permission level. @@ -286,10 +310,18 @@ func AmPermissionLevel(id string) uint16 { * The combined permission level. */ func AmCombinePermissionRole(perm string, role string) uint16 { - p1 := securityRoot.permsMap[perm].level - p2 := securityRoot.roleMap[role].level - if p1 > p2 { - return p1 + pperm, ok := securityRoot.permsMap[perm] + if !ok { + log.Errorf("AmCombinePermissionRole('%s', '%s') - permission not found!", perm, role) + return 0 } - return p2 + prole, ok := securityRoot.roleMap[role] + if !ok { + log.Errorf("AmCombinePermissionRole('%s', '%s') - role not found!", perm, role) + return 0 + } + if pperm.level > prole.level { + return pperm.level + } + return prole.level } diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index 7aa3aad..7e009c2 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -25,7 +25,7 @@ _(italicized items can be deferred)_ - ~~Conferences List honor "hide in list" flag~~ - Community Admin Menu: - ~~Set Community Category~~ - - Membership Control + - ~~Membership Control~~ - ~~E-Mail to All Members~~ - ~~Display Audit Records~~ - Delete Community diff --git a/main.go b/main.go index 839f661..310f32a 100644 --- a/main.go +++ b/main.go @@ -124,6 +124,7 @@ func setupEcho() *echo.Echo { adminGroup.POST("/logo", ui.AmWrap(EditCommunityLogo)) adminGroup.Match(GetAndPost, "/audit", ui.AmWrap(CommunityAudit)) adminGroup.GET("/category", ui.AmWrap(CommunityCategory)) + adminGroup.Match(GetAndPost, "/members", ui.AmWrap(CommunityMembers)) adminGroup.GET("/massmail", ui.AmWrap(CommunityEmailForm)) adminGroup.POST("/massmail", ui.AmWrap(CommunityEmail)) @@ -189,6 +190,7 @@ var ampool *util.WorkerPool // main is Ye Olde Main Function. func main() { + start := time.Now() // Configure the system. config.SetupConfig() closer, err := database.SetupDb() @@ -230,6 +232,9 @@ func main() { database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String())) }() + stime := time.Since(start) + log.Infof("Amsterdam startup sequence completed in %v", stime) + // Start server go func() { if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed { diff --git a/ui/menudefs.yaml b/ui/menudefs.yaml index 8291258..638447b 100644 --- a/ui/menudefs.yaml +++ b/ui/menudefs.yaml @@ -72,7 +72,7 @@ menudefs: permission: "Community.Write" disabled: true - text: "Membership Control" - link: "/TODO/comm/[CID]/admin/members" + link: "/comm/[CID]/admin/members" permission: "Community.ShowAdmin" - text: "E-Mail to All Members" link: "/comm/[CID]/admin/massmail" diff --git a/ui/views/comm_members.jet b/ui/views/comm_members.jet new file mode 100644 index 0000000..e2ea93a --- /dev/null +++ b/ui/views/comm_members.jet @@ -0,0 +1,160 @@ +{* + * 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/. + *} +
+ +
+
+

Set Community Membership:

+ {{ commName }} +
+
+
+ + +
+ + 🔃 + Redisplay Member List + + | + + + Return to Community Administration Menu + +
+ + +
+
+ + + +
+

Find Users:

+
+
+ Display all users whose + +
+ + +
+ + +
+ +
+ +
+
+
+
+
+ +
+
+

+ {{ if mode == "comm" }} + Community Members: + {{ else if mode == "user" }} + Search Results: + {{ end }} +

+ {{ if total > 0 }} +
(Displaying {{ offset + 1 }}-{{ offset + len(resultList) }} of {{ total }})
+ {{ else }} +
(None found)
+ {{ end }} + {{ if isset(showPrev) || isset(showNext) }} +
+
+ + + + + + {{ if isset(showPrev) }} + + {{ end }} + {{ if isset(showNext) }} + + {{ end }} +
+
+ {{ end }} +
+ +
+ + + + + + +
+
+ {{ range _, rx := resultList }} + {{ isHost := (rx.Level == hostRole.Level()) }} +
+ 🟣 + + + + + 🔒 + +
+ {{ end }} +
+ + {{ if isset(updated) }} + ✅ Updated! + {{ end }} +
+
+
+
+ +