community profile and left menu implementation done, not quite working yet

This commit is contained in:
2025-10-16 23:05:34 -06:00
parent 681b30272d
commit 65c739dc57
17 changed files with 412 additions and 72 deletions
+8 -22
View File
@@ -25,27 +25,12 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) {
if err != nil { if err != nil {
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
globals, err := database.AmGlobals() err = ctxt.SetCommunityContext(ctxt.URLParam("cid"))
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"))
if err != nil { if err != nil {
ctxt.SetRC(http.StatusNotFound) ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
member, _, level, err := comm.Membership(me) comm := ctxt.CurrentCommunity()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
effectiveLevel := me.BaseLevel
if member && level > effectiveLevel {
effectiveLevel = level
}
ci, err := comm.ContactInfo() ci, err := comm.ContactInfo()
if err != nil { if err != nil {
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
@@ -55,14 +40,14 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
var cats []*database.Category var cats []*database.Category
if !globalFlags.Get(database.GlobalFlagNoCategories) { if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
cats, err = database.AmGetCategoryHierarchy(comm.CategoryId) cats, err = database.AmGetCategoryHierarchy(comm.CategoryId)
if err != nil { if err != nil {
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
} }
var pvtAddr bool var pvtAddr bool
if database.AmTestPermission("Global.SeeHiddenContactInfo", effectiveLevel) { if ctxt.TestPermission("Global.SeeHiddenContactInfo") {
pvtAddr = false pvtAddr = false
} else { } else {
pvtAddr = ci.PrivateAddr pvtAddr = ci.PrivateAddr
@@ -79,14 +64,14 @@ func ShowCommunity(ctxt ui.AmContext) (string, any, error) {
if comm.LastUpdate != nil { if comm.LastUpdate != nil {
ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", (*comm.LastUpdate).In(tz))) 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) ctxt.VarMap().Set("canJoin", true)
} }
if member && !me.IsAnon { if ctxt.IsMember() && !me.IsAnon {
ctxt.VarMap().Set("canInvite", true) ctxt.VarMap().Set("canInvite", true)
} }
ctxt.VarMap().Set("public", comm.Public()) ctxt.VarMap().Set("public", comm.Public())
if !globalFlags.Get(database.GlobalFlagNoCategories) { if !ctxt.GlobalFlags().Get(database.GlobalFlagNoCategories) {
ctxt.VarMap().Set("categories", cats) ctxt.VarMap().Set("categories", cats)
} }
if comm.Synopsis != nil && *comm.Synopsis != "" { 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.VarMap().Set("homePage", *ci.URL)
} }
ctxt.SetLeftMenu("community")
ctxt.VarMap().Set("amsterdam_pageTitle", "Community Profile: "+comm.Name) ctxt.VarMap().Set("amsterdam_pageTitle", "Community Profile: "+comm.Name)
return "framed_template", "comprofile.jet", nil return "framed_template", "comprofile.jet", nil
} }
+31 -8
View File
@@ -165,16 +165,39 @@ func (c *Community) Membership(u *User) (bool, bool, uint16, error) {
* true if the user has the permission, false if not. * true if the user has the permission, false if not.
* Standard Go error status. * Standard Go error status.
*/ */
func (c *Community) TestPermission(user *User, perm string) (bool, error) { func (c *Community) TestPermission(perm string, level uint16) bool {
member, _, level, err := c.Membership(user) switch perm {
if err != nil { case "Community.Read":
return false, err 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. /* AmGetCommunity returns a reference to the specified community.
+2 -2
View File
@@ -22,8 +22,8 @@ import (
type ContactInfo struct { type ContactInfo struct {
Mutex sync.Mutex Mutex sync.Mutex
ContactId int32 `db:"contactid"` ContactId int32 `db:"contactid"`
GivenName string `db:"given_name"` GivenName *string `db:"given_name"`
FamilyName string `db:"family_name"` FamilyName *string `db:"family_name"`
MiddleInit *string `db:"middle_init"` MiddleInit *string `db:"middle_init"`
Prefix *string `db:"prefix"` Prefix *string `db:"prefix"`
Suffix *string `db:"suffix"` Suffix *string `db:"suffix"`
+22 -1
View File
@@ -254,5 +254,26 @@ func AmRoleList(id string) RoleList {
* true if the permission test is satisfied, false if not. * true if the permission test is satisfied, false if not.
*/ */
func AmTestPermission(id string, level uint16) bool { 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
} }
+1 -1
View File
@@ -27,7 +27,7 @@ type ServiceDef struct {
RequirePermission string `yaml:"requirePermission"` RequirePermission string `yaml:"requirePermission"`
RequireRole string `yaml:"requireRole"` RequireRole string `yaml:"requireRole"`
LinkSequence int `yaml:"linkSequence"` LinkSequence int `yaml:"linkSequence"`
Link int `yaml:"link"` Link string `yaml:"link"`
Title string `yaml:"title"` Title string `yaml:"title"`
} }
+1
View File
@@ -22,6 +22,7 @@ import (
* Standard Go error status. * Standard Go error status.
*/ */
func NotImplPage(ctxt ui.AmContext) (string, any, error) { func NotImplPage(ctxt ui.AmContext) (string, any, error) {
ctxt.SetLeftMenu("top")
ctxt.VarMap().Set("amsterdam_pageTitle", "Function Not Implemented") ctxt.VarMap().Set("amsterdam_pageTitle", "Function Not Implemented")
ctxt.VarMap().Set("path", ctxt.URLPath()) ctxt.VarMap().Set("path", ctxt.URLPath())
return "framed_template", "notimpl.jet", nil return "framed_template", "notimpl.jet", nil
+4 -2
View File
@@ -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")) 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("target", target)
ctxt.VarMap().Set("amsterdam_pageTitle", "New Account User Agreement") ctxt.VarMap().Set("amsterdam_pageTitle", "New Account User Agreement")
ctxt.VarMap().Set("amsterdam_suppressLogin", true) ctxt.VarMap().Set("amsterdam_suppressLogin", true)
@@ -370,13 +371,13 @@ func NewAccount(ctxt ui.AmContext) (string, any, error) {
// create and save contact info // create and save contact info
ci := database.AmNewUserContactInfo(user.Uid) ci := database.AmNewUserContactInfo(user.Uid)
ci.Prefix = dlg.Field("prefix").ValPtr() ci.Prefix = dlg.Field("prefix").ValPtr()
ci.GivenName = dlg.Field("first").Value ci.GivenName = dlg.Field("first").ValPtr()
mid := dlg.Field("mid").Value mid := dlg.Field("mid").Value
if mid == "" { if mid == "" {
mid = " " mid = " "
} }
ci.MiddleInit = &mid ci.MiddleInit = &mid
ci.FamilyName = dlg.Field("last").Value ci.FamilyName = dlg.Field("last").ValPtr()
ci.Suffix = dlg.Field("suffix").ValPtr() ci.Suffix = dlg.Field("suffix").ValPtr()
ci.Locality = dlg.Field("loc").ValPtr() ci.Locality = dlg.Field("loc").ValPtr()
ci.Region = dlg.Field("reg").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("username", user.Username)
msg.AddVariable("password", newpass) msg.AddVariable("password", newpass)
msg.Send() msg.Send()
ctxt.SetLeftMenu("top")
ctxt.VarMap().Set("amsterdam_pageTitle", "Your Password Has Been Changed") ctxt.VarMap().Set("amsterdam_pageTitle", "Your Password Has Been Changed")
return "framed_template", "password_changed.jet", nil return "framed_template", "password_changed.jet", nil
} }
+2
View File
@@ -78,6 +78,8 @@ func main() {
ui.SetupTemplates() ui.SetupTemplates()
closer = ui.SetupSessionManager() closer = ui.SetupSessionManager()
defer closer() defer closer()
closer = ui.SetupAmContext()
defer closer()
// Set up Echo. // Set up Echo.
e := setupEcho() e := setupEcho()
+1
View File
@@ -32,6 +32,7 @@ func SysAdminMenu(ctxt ui.AmContext) (string, any, error) {
ctxt.SetRC(http.StatusForbidden) ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, errors.New("you are not authorized access to this page")) return ui.ErrorPage(ctxt, errors.New("you are not authorized access to this page"))
} }
ctxt.SetLeftMenu("top")
menu := ui.AmMenu("sysadmin") menu := ui.AmMenu("sysadmin")
ctxt.VarMap().Set("menu", menu) ctxt.VarMap().Set("menu", menu)
ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title) ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title)
+2 -1
View File
@@ -63,7 +63,7 @@ func buildCommunitiesSidebox(uid int32, out *RenderedSidebox, in *database.Sideb
out.Items = make([]RenderedSideboxItem, len(l)) out.Items = make([]RenderedSideboxItem, len(l))
for i, c := range l { for i, c := range l {
out.Items[i].Text = c.Name 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].Link = &lk
out.Items[i].Flags = make(map[string]bool) out.Items[i].Flags = make(map[string]bool)
var level uint16 var level uint16
@@ -181,6 +181,7 @@ func TopPage(ctxt ui.AmContext) (string, any, error) {
ctxt.VarMap().Set("sideboxes", rc) ctxt.VarMap().Set("sideboxes", rc)
// Final data set. // Final data set.
ctxt.SetLeftMenu("top")
ctxt.VarMap().Set("amsterdam_genRefresh", true) ctxt.VarMap().Set("amsterdam_genRefresh", true)
return "framed_template", "top.jet", nil return "framed_template", "top.jet", nil
} }
+164 -7
View File
@@ -19,6 +19,7 @@ import (
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session" "github.com/labstack/echo-contrib/session"
@@ -30,12 +31,19 @@ import (
type AmContext interface { type AmContext interface {
ClearLoginCookie() ClearLoginCookie()
ClearSession() ClearSession()
CurrentCommunity() *database.Community
CurrentUser() *database.User CurrentUser() *database.User
CurrentUserId() int32 CurrentUserId() int32
Done()
EffectiveLevel() uint16
FormField(string) string FormField(string) string
FormFieldInt(string) (int, error) FormFieldInt(string) (int, error)
FormFieldIsSet(string) bool FormFieldIsSet(string) bool
FormFile(string) (*multipart.FileHeader, error) FormFile(string) (*multipart.FileHeader, error)
Globals() *database.Globals
GlobalFlags() *util.OptionSet
IsMember() bool
LeftMenu() string
RC() int RC() int
OutputType() string OutputType() string
Parameter(string) string Parameter(string) string
@@ -43,11 +51,14 @@ type AmContext interface {
ReplaceUser(*database.User) ReplaceUser(*database.User)
SaveSession() error SaveSession() error
SubRender(string) ([]byte, error) SubRender(string) ([]byte, error)
SetCommunityContext(string) error
SetLeftMenu(string)
SetLoginCookie(string) SetLoginCookie(string)
SetOutputType(string) SetOutputType(string)
SetRC(int) SetRC(int)
GetScratch(string) any GetScratch(string) any
SetScratch(string, any) SetScratch(string, any)
TestPermission(string) bool
URLParam(string) string URLParam(string) string
URLParamInt(string) (int, error) URLParamInt(string) (int, error)
URLPath() string URLPath() string
@@ -62,6 +73,12 @@ type amContext struct {
outputType string outputType string
scratchpad map[string]any scratchpad map[string]any
session *sessions.Session 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. // ClearLoginCookie overwrites and removes the login cookie.
@@ -77,15 +94,26 @@ func (c *amContext) ClearLoginCookie() {
// ClearSession clears the current session. // ClearSession clears the current session.
func (c *amContext) ClearSession() { func (c *amContext) ClearSession() {
AmResetSession(c.session) 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. // CurrentUser returns the current user from the session.
func (c *amContext) CurrentUser() *database.User { func (c *amContext) CurrentUser() *database.User {
if c.user == nil {
u, err := database.AmGetUser(AmSessionUid(c.session)) u, err := database.AmGetUser(AmSessionUid(c.session))
if err != nil { if err != nil {
log.Errorf("unable to retrieve current user") log.Errorf("unable to retrieve current user")
} }
return u c.user = u
c.effectiveLevel = u.BaseLevel
}
return c.user
} }
// CurrentUserId returns the current user ID. // CurrentUserId returns the current user ID.
@@ -93,6 +121,16 @@ func (c *amContext) CurrentUserId() int32 {
return AmSessionUid(c.session) 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. /* FormField returns the value of a form field from the request.
* Parameters: * Parameters:
* name - The name of the field to retrieve. * 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) 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. // RC returns the HTTP result code for the current operation.
func (c *amContext) RC() int { func (c *amContext) RC() int {
return c.httprc return c.httprc
@@ -168,6 +226,8 @@ func (c *amContext) RemoteIP() string {
*/ */
func (c *amContext) ReplaceUser(u *database.User) { func (c *amContext) ReplaceUser(u *database.User) {
AmSetSessionUser(c.session, u) AmSetSessionUser(c.session, u)
c.user = u
c.effectiveLevel = u.BaseLevel
} }
// SaveSession saves the session link to cookies. // SaveSession saves the session link to cookies.
@@ -201,6 +261,34 @@ func (c *amContext) SubRender(name string) ([]byte, error) {
return buf.Bytes(), err 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. /* SetLoginCookie adds the login cookie to the result output.
* Parameters: * Parameters:
* auth - The auth string to set. * auth - The auth string to set.
@@ -240,6 +328,11 @@ func (c *amContext) SetScratch(name string, val any) {
c.scratchpad[name] = val 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. // URLParam returns the value of a URL parameter.
func (c *amContext) URLParam(name string) string { func (c *amContext) URLParam(name string) string {
return c.echoContext.Param(name) return c.echoContext.Param(name)
@@ -267,22 +360,42 @@ var defoptions *sessions.Options = &sessions.Options{
HttpOnly: true, 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: * Parameters:
* ctxt - The Echo context to be wrapped. * ctxt - The Echo context to be wrapped.
* Returns: * Returns:
* A new Amsterdam context wrapping that context. * A new Amsterdam context wrapping that context.
* Standard Go error status. * Standard Go error status.
*/ */
func NewAmContext(ctxt echo.Context) (AmContext, error) { func AmCreateContext(ctxt echo.Context) (AmContext, error) {
rc := amContext{ rc := freeContext.Get()
echoContext: ctxt, if rc == nil {
rc = &amContext{
httprc: http.StatusOK, httprc: http.StatusOK,
rendervars: make(jet.VarMap), rendervars: make(jet.VarMap),
outputType: "", outputType: "",
scratchpad: nil, 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) sess, err := session.Get("AMSTERDAM_SESSION", ctxt)
if err == nil { if err == nil {
rc.session = sess rc.session = sess
@@ -293,7 +406,14 @@ func NewAmContext(ctxt echo.Context) (AmContext, error) {
AmHitSession(sess) 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. /* AmContextFromEchoContext returns the AmContext associated with an Echo context.
@@ -312,3 +432,40 @@ func AmContextFromEchoContext(ctxt echo.Context) AmContext {
} }
return nil 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
}
}
+3
View File
@@ -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_required", required)
ctxt.VarMap().Set("amsterdam_dialog", d) ctxt.VarMap().Set("amsterdam_dialog", d)
ctxt.VarMap().Set("amsterdam_pageTitle", d.Title) ctxt.VarMap().Set("amsterdam_pageTitle", d.Title)
+88 -2
View File
@@ -11,12 +11,19 @@ package ui
import ( import (
_ "embed" _ "embed"
"fmt"
"slices"
"strconv"
"strings"
"sync"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
lru "github.com/hashicorp/golang-lru"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// MenuItem represents an itrem within a menu definition. // MenuItem represents an item within a menu definition.
type MenuItem struct { type MenuItem struct {
Text string `yaml:"text"` Text string `yaml:"text"`
Link string `yaml:"link"` Link string `yaml:"link"`
@@ -35,10 +42,23 @@ func (mi *MenuItem) Show(ctxt AmContext) bool {
switch mi.P.PermSet { switch mi.P.PermSet {
case "user": case "user":
eperm = u.BaseLevel eperm = u.BaseLevel
case "community":
eperm = ctxt.EffectiveLevel()
default: default:
eperm = database.AmRole("NotInList").Level() eperm = database.AmRole("NotInList").Level()
} }
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) return database.AmTestPermission(mi.Permission, eperm)
case "community":
return ctxt.CurrentCommunity().TestPermission(mi.Permission, eperm)
default:
return false
}
} }
// MenuDefinition represents a full menu definition. // MenuDefinition represents a full menu definition.
@@ -48,6 +68,7 @@ type MenuDefinition struct {
PermSet string `yaml:"permSet"` PermSet string `yaml:"permSet"`
Warning string `yaml:"warning"` Warning string `yaml:"warning"`
Items []MenuItem `yaml:"items"` Items []MenuItem `yaml:"items"`
Tag string
} }
// MenuDefs represents the set of all menu definitions. // MenuDefs represents the set of all menu definitions.
@@ -62,9 +83,19 @@ var initMenuData []byte
// menuDefinitions gives the menu definitions. // menuDefinitions gives the menu definitions.
var menuDefinitions MenuDefs 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. // init loads the menu definitions.
func init() { 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 panic(err) // can't happen
} }
menuDefinitions.table = make(map[string]*MenuDefinition) menuDefinitions.table = make(map[string]*MenuDefinition)
@@ -73,6 +104,7 @@ func init() {
for j := range menuDefinitions.D[i].Items { for j := range menuDefinitions.D[i].Items {
menuDefinitions.D[i].Items[j].P = &(menuDefinitions.D[i]) menuDefinitions.D[i].Items[j].P = &(menuDefinitions.D[i])
} }
menuDefinitions.D[i].Tag = ""
} }
} }
@@ -80,3 +112,57 @@ func init() {
func AmMenu(name string) *MenuDefinition { func AmMenu(name string) *MenuDefinition {
return menuDefinitions.table[name] 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
}
+17 -1
View File
@@ -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) err = ctxt.Render(amctxt.RC(), fmt.Sprintf("%v", data), amctxt)
case "framed_template": case "framed_template":
amctxt.VarMap().Set("amsterdam_innerPage", data) 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) err = ctxt.Render(amctxt.RC(), "frame.jet", amctxt)
default: default:
err = fmt.Errorf("unknown rendering type: %s", command) 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 { func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
return func(ctxt echo.Context) error { return func(ctxt echo.Context) error {
// Create the AmContext. // Create the AmContext.
amctxt, aerr := NewAmContext(ctxt) amctxt, aerr := AmCreateContext(ctxt)
if aerr != nil { if aerr != nil {
ctxt.Logger().Errorf("Session creation error: %v", aerr) ctxt.Logger().Errorf("Session creation error: %v", aerr)
return aerr return aerr
} }
defer amctxt.Done()
// Check IP banning. // Check IP banning.
banmsg, banerr := database.AmTestIPBan(ctxt.RealIP()) banmsg, banerr := database.AmTestIPBan(ctxt.RealIP())
+9 -3
View File
@@ -81,11 +81,17 @@
<div class="flex"> <div class="flex">
<!-- LEFT SIDEBAR --> <!-- LEFT SIDEBAR -->
<div class="w-48 bg-blue-400 p-2"> <div class="w-48 bg-blue-400 p-2">
{{ .SetScratch("__menu", AmMenu("top")) }} {{ range i, m := amsterdam_leftMenus }}
{{ .SubRender("menu_left.jet") | raw }} {{ if i > 0 }}
<div class="mb-2 mt-2">&nbsp;</div> <div class="mb-2 mt-2">&nbsp;</div>
{{ .SetScratch("__menu", AmMenu("fixed")) }} {{ end }}
{{ .SetScratch("__menu", m) }}
{{ if m.Tag == "community" }}
{{ .SubRender("menu_left_comm.jet") | raw }}
{{ else }}
{{ .SubRender("menu_left.jet") | raw }} {{ .SubRender("menu_left.jet") | raw }}
{{ end }}
{{ end }}
</div> </div>
<!-- MAIN CONTENT --> <!-- MAIN CONTENT -->
+31
View File
@@ -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() }}
<div class="mb-2 mt-2">
<div class="mb-1">
<img src="/img/builtin/default-community.jpg"
alt="{{ comm.Name }}" class="w-28 h-16 rounded">
</div>
<div class="font-bold mb-1">{{ menu.Title }}</div>
{{ ctxt := . }}
{{ range menu.Items }}
{{ if .Show(ctxt) }}
{{ if .Disabled }}
<div class="text-gray-500 mb-1">{{ .Text }}</div>
{{ else }}
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900">{{ .Text }}</a>
{{ end }}
{{ end }}
{{ end }}
{{ if .IsMember() }}
<div class="mb-1">&nbsp;</div>
<div class="mb-1"><a href="/TODO/comm/{{ comm.Alias }}/unjoin">Unjoin</a></div>
{{ end }}
</div>
+10 -6
View File
@@ -63,9 +63,9 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) {
if err == nil { if err == nil {
dlg.Field("remind").Value = u.PassReminder dlg.Field("remind").Value = u.PassReminder
dlg.Field("prefix").SetVal(ci.Prefix) 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("mid").SetVal(ci.MiddleInit)
dlg.Field("last").Value = ci.FamilyName dlg.Field("last").SetVal(ci.FamilyName)
dlg.Field("suffix").SetVal(ci.Suffix) dlg.Field("suffix").SetVal(ci.Suffix)
dlg.Field("company").SetVal(ci.Company) dlg.Field("company").SetVal(ci.Company)
dlg.Field("addr1").SetVal(ci.Addr1) dlg.Field("addr1").SetVal(ci.Addr1)
@@ -141,9 +141,9 @@ func EditProfile(ctxt ui.AmContext) (string, any, error) {
if err == nil { if err == nil {
nci := ci.Clone() nci := ci.Clone()
nci.Prefix = dlg.Field("prefix").ValPtr() 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.MiddleInit = dlg.Field("mid").ValPtr()
nci.FamilyName = dlg.Field("last").Value nci.FamilyName = dlg.Field("last").ValPtr()
nci.Suffix = dlg.Field("suffix").ValPtr() nci.Suffix = dlg.Field("suffix").ValPtr()
nci.Company = dlg.Field("company").ValPtr() nci.Company = dlg.Field("company").ValPtr()
nci.Addr1 = dlg.Field("addr1").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 != "" { if ci.Prefix != nil && *ci.Prefix != "" {
b.WriteString(*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 != " " { if ci.MiddleInit != nil && *ci.MiddleInit != "" && *ci.MiddleInit != " " {
b.WriteString(" " + *ci.MiddleInit + ".") b.WriteString(" " + *ci.MiddleInit + ".")
} }
b.WriteString(" " + ci.FamilyName) if ci.FamilyName != nil {
b.WriteString(" " + *ci.FamilyName)
}
if ci.Suffix != nil && *ci.Suffix != "" { if ci.Suffix != nil && *ci.Suffix != "" {
b.WriteString(" " + *ci.Suffix) b.WriteString(" " + *ci.Suffix)
} }