community profile and left menu implementation done, not quite working yet
This commit is contained in:
+177
-20
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+89
-3
@@ -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
|
||||
}
|
||||
|
||||
+17
-1
@@ -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())
|
||||
|
||||
+11
-5
@@ -81,11 +81,17 @@
|
||||
<div class="flex">
|
||||
<!-- LEFT SIDEBAR -->
|
||||
<div class="w-48 bg-blue-400 p-2">
|
||||
{{ .SetScratch("__menu", AmMenu("top")) }}
|
||||
{{ .SubRender("menu_left.jet") | raw }}
|
||||
<div class="mb-2 mt-2"> </div>
|
||||
{{ .SetScratch("__menu", AmMenu("fixed")) }}
|
||||
{{ .SubRender("menu_left.jet") | raw }}
|
||||
{{ range i, m := amsterdam_leftMenus }}
|
||||
{{ if i > 0 }}
|
||||
<div class="mb-2 mt-2"> </div>
|
||||
{{ end }}
|
||||
{{ .SetScratch("__menu", m) }}
|
||||
{{ if m.Tag == "community" }}
|
||||
{{ .SubRender("menu_left_comm.jet") | raw }}
|
||||
{{ else }}
|
||||
{{ .SubRender("menu_left.jet") | raw }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
|
||||
@@ -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"> </div>
|
||||
<div class="mb-1"><a href="/TODO/comm/{{ comm.Alias }}/unjoin">Unjoin</a></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
Reference in New Issue
Block a user