diff --git a/community.go b/community.go index 2183e38..69bd019 100644 --- a/community.go +++ b/community.go @@ -25,27 +25,12 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) { if err != nil { return ui.ErrorPage(ctxt, err) } - globals, err := database.AmGlobals() - if err != nil { - return ui.ErrorPage(ctxt, err) - } - globalFlags, err := globals.Flags() - if err != nil { - return ui.ErrorPage(ctxt, err) - } - comm, err := database.AmGetCommunityFromParam(ctxt.URLParam("cid")) + err = ctxt.SetCommunityContext(ctxt.URLParam("cid")) if err != nil { ctxt.SetRC(http.StatusNotFound) return ui.ErrorPage(ctxt, err) } - member, _, level, err := comm.Membership(me) - if err != nil { - return ui.ErrorPage(ctxt, err) - } - effectiveLevel := me.BaseLevel - if member && level > effectiveLevel { - effectiveLevel = level - } + comm := ctxt.CurrentCommunity() ci, err := comm.ContactInfo() if err != nil { return ui.ErrorPage(ctxt, err) @@ -55,14 +40,14 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } var cats []*database.Category - if !globalFlags.Get(database.GlobalFlagNoCategories) { + if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { cats, err = database.AmGetCategoryHierarchy(comm.CategoryId) if err != nil { return ui.ErrorPage(ctxt, err) } } var pvtAddr bool - if database.AmTestPermission("Global.SeeHiddenContactInfo", effectiveLevel) { + if ctxt.TestPermission("Global.SeeHiddenContactInfo") { pvtAddr = false } else { pvtAddr = ci.PrivateAddr @@ -79,14 +64,14 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) { if comm.LastUpdate != nil { ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", (*comm.LastUpdate).In(tz))) } - if !member && effectiveLevel >= comm.JoinLevel { + if !ctxt.IsMember() && ctxt.EffectiveLevel() >= comm.JoinLevel { ctxt.VarMap().Set("canJoin", true) } - if member && !me.IsAnon { + if ctxt.IsMember() && !me.IsAnon { ctxt.VarMap().Set("canInvite", true) } ctxt.VarMap().Set("public", comm.Public()) - if !globalFlags.Get(database.GlobalFlagNoCategories) { + if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) { ctxt.VarMap().Set("categories", cats) } if comm.Synopsis != nil && *comm.Synopsis != "" { @@ -131,6 +116,7 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) { ctxt.VarMap().Set("homePage", *ci.URL) } + ctxt.SetLeftMenu("community") ctxt.VarMap().Set("amsterdam_pageTitle", "Community Profile: "+comm.Name) return "framed_template", "comprofile.jet", nil } diff --git a/database/community.go b/database/community.go index 8370322..ce93325 100644 --- a/database/community.go +++ b/database/community.go @@ -165,16 +165,39 @@ func (c *Community) Membership(u *User) (bool, bool, uint16, error) { * true if the user has the permission, false if not. * Standard Go error status. */ -func (c *Community) TestPermission(user *User, perm string) (bool, error) { - member, _, level, err := c.Membership(user) - if err != nil { - return false, err +func (c *Community) TestPermission(perm string, level uint16) bool { + switch perm { + case "Community.Read": + return level >= c.ReadLevel + case "Community.Write": + return level >= c.WriteLevel + case "Community.Create": + return level >= c.CreateLevel + case "Community.Delete": + return level >= c.DeleteLevel + case "Community.Join": + return level >= c.JoinLevel + default: + return AmTestPermission(perm, level) } - effectiveLevel := user.BaseLevel - if member && level > effectiveLevel { - effectiveLevel = level +} + +// PermissionLevel returns trhe permission level for a permission name. +func (c *Community) PermissionLevel(perm string) uint16 { + switch perm { + case "Community.Read": + return c.ReadLevel + case "Community.Write": + return c.WriteLevel + case "Community.Create": + return c.CreateLevel + case "Community.Delete": + return c.DeleteLevel + case "Community.Join": + return c.JoinLevel + default: + return AmPermissionLevel(perm) } - return AmTestPermission(perm, effectiveLevel), nil } /* AmGetCommunity returns a reference to the specified community. diff --git a/database/contactinfo.go b/database/contactinfo.go index 38a7c99..ffe8402 100644 --- a/database/contactinfo.go +++ b/database/contactinfo.go @@ -22,8 +22,8 @@ import ( type ContactInfo struct { Mutex sync.Mutex ContactId int32 `db:"contactid"` - GivenName string `db:"given_name"` - FamilyName string `db:"family_name"` + GivenName *string `db:"given_name"` + FamilyName *string `db:"family_name"` MiddleInit *string `db:"middle_init"` Prefix *string `db:"prefix"` Suffix *string `db:"suffix"` diff --git a/database/security.go b/database/security.go index 7b3b1d5..1a24352 100644 --- a/database/security.go +++ b/database/security.go @@ -254,5 +254,26 @@ 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 + return securityRoot.permsMap[id].level <= level +} + +// AmPermissionLevel returns a level value for a permission. +func AmPermissionLevel(id string) uint16 { + return securityRoot.permsMap[id].level +} + +/* AmCombinePermissionRole combines a permission and a role into a single permission level. + * Parameters: + * perm - Permission to use. + * role - Role to use. + * Returns: + * 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 + } + return p2 } diff --git a/database/services.go b/database/services.go index b794c27..3defcb9 100644 --- a/database/services.go +++ b/database/services.go @@ -27,7 +27,7 @@ type ServiceDef struct { RequirePermission string `yaml:"requirePermission"` RequireRole string `yaml:"requireRole"` LinkSequence int `yaml:"linkSequence"` - Link int `yaml:"link"` + Link string `yaml:"link"` Title string `yaml:"title"` } diff --git a/errors.go b/errors.go index fa0f0fa..87834ec 100644 --- a/errors.go +++ b/errors.go @@ -22,6 +22,7 @@ import ( * Standard Go error status. */ func NotImplPage(ctxt ui.AmContext) (string, any, error) { + ctxt.SetLeftMenu("top") ctxt.VarMap().Set("amsterdam_pageTitle", "Function Not Implemented") ctxt.VarMap().Set("path", ctxt.URLPath()) return "framed_template", "notimpl.jet", nil diff --git a/login.go b/login.go index fb95bfd..4e8cc22 100644 --- a/login.go +++ b/login.go @@ -289,6 +289,7 @@ func NewAccountUserAgreement(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, errors.New("you cannot create a new account while logged in on an existing one. You must log out first")) } + ctxt.SetLeftMenu("top") ctxt.VarMap().Set("target", target) ctxt.VarMap().Set("amsterdam_pageTitle", "New Account User Agreement") ctxt.VarMap().Set("amsterdam_suppressLogin", true) @@ -370,13 +371,13 @@ func NewAccount(ctxt ui.AmContext) (string, any, error) { // create and save contact info ci := database.AmNewUserContactInfo(user.Uid) ci.Prefix = dlg.Field("prefix").ValPtr() - ci.GivenName = dlg.Field("first").Value + ci.GivenName = dlg.Field("first").ValPtr() mid := dlg.Field("mid").Value if mid == "" { mid = " " } ci.MiddleInit = &mid - ci.FamilyName = dlg.Field("last").Value + ci.FamilyName = dlg.Field("last").ValPtr() ci.Suffix = dlg.Field("suffix").ValPtr() ci.Locality = dlg.Field("loc").ValPtr() ci.Region = dlg.Field("reg").ValPtr() @@ -446,6 +447,7 @@ func PasswordRecovery(ctxt ui.AmContext) (string, any, error) { msg.AddVariable("username", user.Username) msg.AddVariable("password", newpass) msg.Send() + ctxt.SetLeftMenu("top") ctxt.VarMap().Set("amsterdam_pageTitle", "Your Password Has Been Changed") return "framed_template", "password_changed.jet", nil } diff --git a/main.go b/main.go index ecbaf76..67a5072 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,8 @@ func main() { ui.SetupTemplates() closer = ui.SetupSessionManager() defer closer() + closer = ui.SetupAmContext() + defer closer() // Set up Echo. e := setupEcho() diff --git a/sysadmin.go b/sysadmin.go index 5269a9f..58aa681 100644 --- a/sysadmin.go +++ b/sysadmin.go @@ -32,6 +32,7 @@ func SysAdminMenu(ctxt ui.AmContext) (string, any, error) { ctxt.SetRC(http.StatusForbidden) return ui.ErrorPage(ctxt, errors.New("you are not authorized access to this page")) } + ctxt.SetLeftMenu("top") menu := ui.AmMenu("sysadmin") ctxt.VarMap().Set("menu", menu) ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title) diff --git a/top.go b/top.go index a6afc69..962b983 100644 --- a/top.go +++ b/top.go @@ -63,7 +63,7 @@ func buildCommunitiesSidebox(uid int32, out *RenderedSidebox, in *database.Sideb out.Items = make([]RenderedSideboxItem, len(l)) for i, c := range l { out.Items[i].Text = c.Name - lk := fmt.Sprintf("/TODO/community/%s", c.Alias) + lk := fmt.Sprintf("/comm/%s/profile", c.Alias) out.Items[i].Link = &lk out.Items[i].Flags = make(map[string]bool) var level uint16 @@ -181,6 +181,7 @@ func TopPage(ctxt ui.AmContext) (string, any, error) { ctxt.VarMap().Set("sideboxes", rc) // Final data set. + ctxt.SetLeftMenu("top") ctxt.VarMap().Set("amsterdam_genRefresh", true) return "framed_template", "top.jet", nil } diff --git a/ui/amcontext.go b/ui/amcontext.go index 9c83610..0df599d 100644 --- a/ui/amcontext.go +++ b/ui/amcontext.go @@ -19,6 +19,7 @@ import ( "git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/util" "github.com/CloudyKit/jet/v6" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" @@ -30,12 +31,19 @@ import ( type AmContext interface { ClearLoginCookie() ClearSession() + CurrentCommunity() *database.Community CurrentUser() *database.User CurrentUserId() int32 + Done() + EffectiveLevel() uint16 FormField(string) string FormFieldInt(string) (int, error) FormFieldIsSet(string) bool FormFile(string) (*multipart.FileHeader, error) + Globals() *database.Globals + GlobalFlags() *util.OptionSet + IsMember() bool + LeftMenu() string RC() int OutputType() string Parameter(string) string @@ -43,11 +51,14 @@ type AmContext interface { ReplaceUser(*database.User) SaveSession() error SubRender(string) ([]byte, error) + SetCommunityContext(string) error + SetLeftMenu(string) SetLoginCookie(string) SetOutputType(string) SetRC(int) GetScratch(string) any SetScratch(string, any) + TestPermission(string) bool URLParam(string) string URLParamInt(string) (int, error) URLPath() string @@ -56,12 +67,18 @@ type AmContext interface { // amContext is the internal structure that implements AmContext. type amContext struct { - echoContext echo.Context - httprc int - rendervars jet.VarMap - outputType string - scratchpad map[string]any - session *sessions.Session + echoContext echo.Context + httprc int + rendervars jet.VarMap + outputType string + scratchpad map[string]any + session *sessions.Session + globals *database.Globals + globalFlags *util.OptionSet + user *database.User + effectiveLevel uint16 + community *database.Community + isMember bool } // ClearLoginCookie overwrites and removes the login cookie. @@ -77,15 +94,26 @@ func (c *amContext) ClearLoginCookie() { // ClearSession clears the current session. func (c *amContext) ClearSession() { AmResetSession(c.session) + c.user = nil + c.effectiveLevel = 0 +} + +// CurrentCommunity returns the current community, if one's been set. +func (c *amContext) CurrentCommunity() *database.Community { + return c.community } // CurrentUser returns the current user from the session. func (c *amContext) CurrentUser() *database.User { - u, err := database.AmGetUser(AmSessionUid(c.session)) - if err != nil { - log.Errorf("unable to retrieve current user") + if c.user == nil { + u, err := database.AmGetUser(AmSessionUid(c.session)) + if err != nil { + log.Errorf("unable to retrieve current user") + } + c.user = u + c.effectiveLevel = u.BaseLevel } - return u + return c.user } // CurrentUserId returns the current user ID. @@ -93,6 +121,16 @@ func (c *amContext) CurrentUserId() int32 { return AmSessionUid(c.session) } +// Done signals that we're done with this context and it can be recycled. +func (c *amContext) Done() { + amContextRecycleBin <- c +} + +// EffectiveLevel returns the user's effective access level (in terms of current community, if any). +func (c *amContext) EffectiveLevel() uint16 { + return c.effectiveLevel +} + /* FormField returns the value of a form field from the request. * Parameters: * name - The name of the field to retrieve. @@ -133,6 +171,26 @@ func (c *amContext) FormFile(name string) (*multipart.FileHeader, error) { return c.echoContext.FormFile(name) } +// Globals returns a reference to the database globals. +func (c *amContext) Globals() *database.Globals { + return c.globals +} + +// GlobalFlags returns a reference to the database global flags. +func (c *amContext) GlobalFlags() *util.OptionSet { + return c.globalFlags +} + +// IsMember returns true if the user is a member of the current community. +func (c *amContext) IsMember() bool { + return c.isMember +} + +// LeftMenu returns the current left menu selector. +func (c *amContext) LeftMenu() string { + return c.session.Values["leftMenu"].(string) +} + // RC returns the HTTP result code for the current operation. func (c *amContext) RC() int { return c.httprc @@ -168,6 +226,8 @@ func (c *amContext) RemoteIP() string { */ func (c *amContext) ReplaceUser(u *database.User) { AmSetSessionUser(c.session, u) + c.user = u + c.effectiveLevel = u.BaseLevel } // SaveSession saves the session link to cookies. @@ -201,6 +261,34 @@ func (c *amContext) SubRender(name string) ([]byte, error) { return buf.Bytes(), err } +/* SetCommunityContext establishes the community context from a (ID or alias) parameter. + * Parameters: + * param - String parameter selecting the community. + * Returns: + * Standard Go error status. + */ +func (c *amContext) SetCommunityContext(param string) error { + comm, err := database.AmGetCommunityFromParam(param) + if err != nil { + return err + } + mbr, _, level, err := comm.Membership(c.CurrentUser()) + if err != nil { + return err + } + c.community = comm + c.isMember = mbr + if level > c.effectiveLevel { + c.effectiveLevel = level + } + return nil +} + +// SetLeftMenu sets the current topmost left menu name value. +func (c *amContext) SetLeftMenu(name string) { + c.session.Values["leftMenu"] = name +} + /* SetLoginCookie adds the login cookie to the result output. * Parameters: * auth - The auth string to set. @@ -240,6 +328,11 @@ func (c *amContext) SetScratch(name string, val any) { c.scratchpad[name] = val } +// TestPermission tests the current user against permissions. +func (c *amContext) TestPermission(perm string) bool { + return database.AmTestPermission(perm, c.effectiveLevel) +} + // URLParam returns the value of a URL parameter. func (c *amContext) URLParam(name string) string { return c.echoContext.Param(name) @@ -267,22 +360,42 @@ var defoptions *sessions.Options = &sessions.Options{ HttpOnly: true, } -/* NewAmContext creates a new AmContext wrapping the Echo context. +// freeContext is a free list for amContext structures. +var freeContext util.FreeList[amContext] + +// amContextRecycleBin is the channel we put contexts on to be recycled. +var amContextRecycleBin chan *amContext + +/* AmCreateContext creates a new AmContext wrapping the Echo context. * Parameters: * ctxt - The Echo context to be wrapped. * Returns: * A new Amsterdam context wrapping that context. * Standard Go error status. */ -func NewAmContext(ctxt echo.Context) (AmContext, error) { - rc := amContext{ - echoContext: ctxt, - httprc: http.StatusOK, - rendervars: make(jet.VarMap), - outputType: "", - scratchpad: nil, +func AmCreateContext(ctxt echo.Context) (AmContext, error) { + rc := freeContext.Get() + if rc == nil { + rc = &amContext{ + httprc: http.StatusOK, + rendervars: make(jet.VarMap), + outputType: "", + scratchpad: nil, + } } - ctxt.Set("amsterdam_context", &rc) + + var err error + if rc.globals, err = database.AmGlobals(); err != nil { + amContextRecycleBin <- rc + return nil, err + } + if rc.globalFlags, err = rc.globals.Flags(); err != nil { + amContextRecycleBin <- rc + return nil, err + } + + rc.echoContext = ctxt + ctxt.Set("amsterdam_context", rc) sess, err := session.Get("AMSTERDAM_SESSION", ctxt) if err == nil { rc.session = sess @@ -293,7 +406,14 @@ func NewAmContext(ctxt echo.Context) (AmContext, error) { AmHitSession(sess) } } - return &rc, err + rc.user, err = database.AmGetUser(AmSessionUid(sess)) + if err == nil { + rc.effectiveLevel = rc.user.BaseLevel + } else { + rc.user = nil + rc.effectiveLevel = database.AmRole("NotInList").Level() + } + return rc, err } /* AmContextFromEchoContext returns the AmContext associated with an Echo context. @@ -312,3 +432,40 @@ func AmContextFromEchoContext(ctxt echo.Context) AmContext { } return nil } + +// contextRecycler is the task that recycles context blocks. +func contextRecycler(incoming chan *amContext, done chan bool) { + for c := range incoming { + c.echoContext = nil + c.httprc = http.StatusOK + for k := range c.rendervars { + delete(c.rendervars, k) + } + c.outputType = "" + if c.scratchpad != nil { + for k := range c.scratchpad { + delete(c.scratchpad, k) + } + } + c.session = nil + c.globals = nil + c.globalFlags = nil + c.user = nil + c.effectiveLevel = 0 + c.community = nil + c.isMember = false + freeContext.Put(c) + } + done <- true +} + +// SetupAmContext starts the recycler for contexts. +func SetupAmContext() func() { + amContextRecycleBin = make(chan *amContext, 16) + done := make(chan bool) + go contextRecycler(amContextRecycleBin, done) + return func() { + close(amContextRecycleBin) + <-done + } +} diff --git a/ui/dialog.go b/ui/dialog.go index 546127c..4ce016c 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -226,6 +226,9 @@ func (d *Dialog) Render(ctxt AmContext) (string, any, error) { } } } + if d.MenuSelector != "" && d.MenuSelector != "nochange" { + ctxt.SetLeftMenu(d.MenuSelector) + } ctxt.VarMap().Set("amsterdam_required", required) ctxt.VarMap().Set("amsterdam_dialog", d) ctxt.VarMap().Set("amsterdam_pageTitle", d.Title) diff --git a/ui/menus.go b/ui/menus.go index 8cc1aa0..540ec41 100644 --- a/ui/menus.go +++ b/ui/menus.go @@ -11,12 +11,19 @@ package ui import ( _ "embed" + "fmt" + "slices" + "strconv" + "strings" + "sync" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/util" + lru "github.com/hashicorp/golang-lru" "gopkg.in/yaml.v3" ) -// MenuItem represents an itrem within a menu definition. +// MenuItem represents an item within a menu definition. type MenuItem struct { Text string `yaml:"text"` Link string `yaml:"link"` @@ -35,10 +42,23 @@ func (mi *MenuItem) Show(ctxt AmContext) bool { switch mi.P.PermSet { case "user": eperm = u.BaseLevel + case "community": + eperm = ctxt.EffectiveLevel() default: eperm = database.AmRole("NotInList").Level() } - return database.AmTestPermission(mi.Permission, eperm) + if util.IsNumeric(mi.Permission) { + v, _ := strconv.Atoi(mi.Permission) + return uint16(v) <= eperm + } + switch mi.P.PermSet { + case "user": + return database.AmTestPermission(mi.Permission, eperm) + case "community": + return ctxt.CurrentCommunity().TestPermission(mi.Permission, eperm) + default: + return false + } } // MenuDefinition represents a full menu definition. @@ -48,6 +68,7 @@ type MenuDefinition struct { PermSet string `yaml:"permSet"` Warning string `yaml:"warning"` Items []MenuItem `yaml:"items"` + Tag string } // MenuDefs represents the set of all menu definitions. @@ -62,9 +83,19 @@ var initMenuData []byte // menuDefinitions gives the menu definitions. var menuDefinitions MenuDefs +// Cache of community menus. +var menuCache *lru.Cache + +// Mutex controlling access to the cache. +var menuCacheMutex sync.Mutex + // init loads the menu definitions. func init() { - if err := yaml.Unmarshal(initMenuData, &menuDefinitions); err != nil { + var err error + if menuCache, err = lru.New(100); err != nil { + panic(err) + } + if err = yaml.Unmarshal(initMenuData, &menuDefinitions); err != nil { panic(err) // can't happen } menuDefinitions.table = make(map[string]*MenuDefinition) @@ -73,6 +104,7 @@ func init() { for j := range menuDefinitions.D[i].Items { menuDefinitions.D[i].Items[j].P = &(menuDefinitions.D[i]) } + menuDefinitions.D[i].Tag = "" } } @@ -80,3 +112,57 @@ func init() { func AmMenu(name string) *MenuDefinition { return menuDefinitions.table[name] } + +/* AmBuildCommunityMenu buids a community menu for the specified community. + * Parameters: + * comm - The community to build the menu for. + * Returns: + * The new menu definition. + * Standard Go error status. + */ +func AmBuildCommunityMenu(comm *database.Community) (*MenuDefinition, error) { + menuCacheMutex.Lock() + defer menuCacheMutex.Unlock() + m, ok := menuCache.Get(comm.Id) + if ok { + return m.(*MenuDefinition), nil + } + sdef, err := database.AmGetCommunityServices(comm.Id) + if err != nil { + return nil, err + } + slices.SortFunc(sdef, func(a, b *database.ServiceDef) int { + return a.LinkSequence - b.LinkSequence + }) + mia := make([]MenuItem, len(sdef)) + for i, sd := range sdef { + mia[i].Text = sd.Title + mia[i].Link = strings.ReplaceAll(sd.Link, "[CID]", comm.Alias) + mia[i].Disabled = false + if sd.RequirePermission == "" { + if sd.RequireRole == "" { + mia[i].Permission = "" + } else { + mia[i].Permission = fmt.Sprintf("%d", database.AmRole(sd.RequireRole).Level()) + } + } else if sd.RequireRole == "" { + mia[i].Permission = sd.RequirePermission + } else { + v1 := comm.PermissionLevel(sd.RequirePermission) + v2 := database.AmRole(sd.RequireRole).Level() + if v2 > v1 { + v1 = v2 + } + mia[i].Permission = fmt.Sprintf("%d", v1) + } + } + md := MenuDefinition{ + ID: "community", + Title: comm.Name, + PermSet: "community", + Items: mia, + Tag: "community", + } + menuCache.Add(comm.Id, &md) + return &md, nil +} diff --git a/ui/render_wrap.go b/ui/render_wrap.go index fc83ebf..06ee36f 100644 --- a/ui/render_wrap.go +++ b/ui/render_wrap.go @@ -34,6 +34,21 @@ func sendPageData(ctxt echo.Context, amctxt AmContext, command string, data any) err = ctxt.Render(amctxt.RC(), fmt.Sprintf("%v", data), amctxt) case "framed_template": amctxt.VarMap().Set("amsterdam_innerPage", data) + menus := make([]*MenuDefinition, 2) + switch amctxt.LeftMenu() { + case "top": + menus[0] = AmMenu("top") + case "community": + md, err := AmBuildCommunityMenu(amctxt.CurrentCommunity()) + if err != nil { + return err + } + menus[0] = md + default: + return fmt.Errorf("unknown left menu context: %s", amctxt.LeftMenu()) + } + menus[1] = AmMenu("fixed") + amctxt.VarMap().Set("amsterdam_leftMenus", menus) err = ctxt.Render(amctxt.RC(), "frame.jet", amctxt) default: err = fmt.Errorf("unknown rendering type: %s", command) @@ -69,11 +84,12 @@ func ErrorPage(ctxt AmContext, input_err error) (string, any, error) { func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc { return func(ctxt echo.Context) error { // Create the AmContext. - amctxt, aerr := NewAmContext(ctxt) + amctxt, aerr := AmCreateContext(ctxt) if aerr != nil { ctxt.Logger().Errorf("Session creation error: %v", aerr) return aerr } + defer amctxt.Done() // Check IP banning. banmsg, banerr := database.AmTestIPBan(ctxt.RealIP()) diff --git a/ui/views/frame.jet b/ui/views/frame.jet index d03f3ba..e0b9048 100644 --- a/ui/views/frame.jet +++ b/ui/views/frame.jet @@ -81,11 +81,17 @@
- {{ .SetScratch("__menu", AmMenu("top")) }} - {{ .SubRender("menu_left.jet") | raw }} -
 
- {{ .SetScratch("__menu", AmMenu("fixed")) }} - {{ .SubRender("menu_left.jet") | raw }} + {{ range i, m := amsterdam_leftMenus }} + {{ if i > 0 }} +
 
+ {{ end }} + {{ .SetScratch("__menu", m) }} + {{ if m.Tag == "community" }} + {{ .SubRender("menu_left_comm.jet") | raw }} + {{ else }} + {{ .SubRender("menu_left.jet") | raw }} + {{ end }} + {{ end }}
diff --git a/ui/views/menu_left_comm.jet b/ui/views/menu_left_comm.jet new file mode 100644 index 0000000..dd51a3b --- /dev/null +++ b/ui/views/menu_left_comm.jet @@ -0,0 +1,31 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025 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/. + *} +{{ menu := .GetScratch("__menu") }} +{{ comm := .CurrentCommunity() }} +
+
+ {{ comm.Name }} +
+
{{ menu.Title }}
+ {{ ctxt := . }} + {{ range menu.Items }} + {{ if .Show(ctxt) }} + {{ if .Disabled }} +
{{ .Text }}
+ {{ else }} + {{ .Text }} + {{ end }} + {{ end }} + {{ end }} + {{ if .IsMember() }} +
 
+ + {{ end }} +
diff --git a/userdata.go b/userdata.go index 44796c3..f2bebd8 100644 --- a/userdata.go +++ b/userdata.go @@ -63,9 +63,9 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) { if err == nil { dlg.Field("remind").Value = u.PassReminder dlg.Field("prefix").SetVal(ci.Prefix) - dlg.Field("first").Value = ci.GivenName + dlg.Field("first").SetVal(ci.GivenName) dlg.Field("mid").SetVal(ci.MiddleInit) - dlg.Field("last").Value = ci.FamilyName + dlg.Field("last").SetVal(ci.FamilyName) dlg.Field("suffix").SetVal(ci.Suffix) dlg.Field("company").SetVal(ci.Company) dlg.Field("addr1").SetVal(ci.Addr1) @@ -141,9 +141,9 @@ func EditProfile(ctxt ui.AmContext) (string, any, error) { if err == nil { nci := ci.Clone() nci.Prefix = dlg.Field("prefix").ValPtr() - nci.GivenName = dlg.Field("first").Value + nci.GivenName = dlg.Field("first").ValPtr() nci.MiddleInit = dlg.Field("mid").ValPtr() - nci.FamilyName = dlg.Field("last").Value + nci.FamilyName = dlg.Field("last").ValPtr() nci.Suffix = dlg.Field("suffix").ValPtr() nci.Company = dlg.Field("company").ValPtr() nci.Addr1 = dlg.Field("addr1").ValPtr() @@ -369,11 +369,15 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) { if ci.Prefix != nil && *ci.Prefix != "" { b.WriteString(*ci.Prefix + " ") } - b.WriteString(ci.GivenName) + if ci.GivenName != nil { + b.WriteString(*ci.GivenName) + } if ci.MiddleInit != nil && *ci.MiddleInit != "" && *ci.MiddleInit != " " { b.WriteString(" " + *ci.MiddleInit + ".") } - b.WriteString(" " + ci.FamilyName) + if ci.FamilyName != nil { + b.WriteString(" " + *ci.FamilyName) + } if ci.Suffix != nil && *ci.Suffix != "" { b.WriteString(" " + *ci.Suffix) }