working on session management and the Users Online sidebox (not quite there yet)

This commit is contained in:
2025-10-09 22:42:56 -06:00
parent 782fba2c32
commit 560afa47bd
9 changed files with 246 additions and 34 deletions
+7 -8
View File
@@ -75,15 +75,12 @@ func (c *amContext) ClearLoginCookie() {
// ClearSession clears the current session.
func (c *amContext) ClearSession() {
for k := range c.session.Values {
delete(c.session.Values, k)
}
setupAmSession(c.session)
AmResetSession(c.session)
}
// CurrentUser returns the current user from the session.
func (c *amContext) CurrentUser() *database.User {
u, err := database.AmGetUser(c.session.Values["user_id"].(int32))
u, err := database.AmGetUser(AmSessionUid(c.session))
if err != nil {
log.Errorf("unable to retrieve current user")
}
@@ -92,7 +89,7 @@ func (c *amContext) CurrentUser() *database.User {
// CurrentUserId returns the current user ID.
func (c *amContext) CurrentUserId() int32 {
return c.session.Values["user_id"].(int32)
return AmSessionUid(c.session)
}
/* FormField returns the value of a form field from the request.
@@ -174,7 +171,7 @@ func (c *amContext) Render(name string) error {
* u - New user to associate with the context.
*/
func (c *amContext) ReplaceUser(u *database.User) {
c.session.Values["user_id"] = u.Uid
AmSetSessionUser(c.session, u)
}
// SaveSession saves the session link to cookies.
@@ -295,7 +292,9 @@ func NewAmContext(ctxt echo.Context) (AmContext, error) {
rc.session = sess
sess.Options = defoptions
if sess.IsNew {
setupAmSession(sess)
AmSessionFirstTime(sess)
} else {
AmHitSession(sess)
}
}
return &rc, err
+169 -10
View File
@@ -11,8 +11,15 @@
package ui
import (
"encoding/gob"
"slices"
"sync"
"sync/atomic"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/google/uuid"
"github.com/gorilla/sessions"
"github.com/quasoft/memstore"
log "github.com/sirupsen/logrus"
@@ -21,17 +28,169 @@ import (
// SessionStore is the Gorilla session store used by Amsterdam.
var SessionStore sessions.Store
// SetupSessionManager sets up the session manager.
func SetupSessionManager() {
SessionStore = memstore.NewMemStore([]byte(config.GlobalConfig.Rendering.CookieKey))
// sessionTable is the global map of all sessions.
var sessionTable map[string]*sessions.Session
// sessionTableMax is the maximum number of entries in the session table.
var sessionTableMax int = 0
// sessionTableMutex is the mutex for the session table.
var sessionTableMutex sync.RWMutex
// sessionExpiry is the amount of time before a session expires.
var sessionExpiry time.Duration
// sweepRunning is the running flag for session sweeping.
var sweepRunning atomic.Bool
// sweepSessions sweeps through the sessions table and removes any expired sessions.
func sweepSessions(tick <-chan time.Time, done chan bool) {
for range tick {
if sweepRunning.Load() {
// phase 1 - identify expired sessions
sessionTableMutex.RLock()
zap := make([]string, 0, len(sessionTable))
for k, v := range sessionTable {
lastTime := v.Values["lasthit"].(time.Time)
if time.Since(lastTime) > sessionExpiry {
zap = append(zap, k)
}
}
sessionTableMutex.RUnlock()
// phase 2 - get rid of the expired sessions
for _, k := range zap {
sessionTableMutex.Lock()
s := sessionTable[k]
delete(sessionTable, k)
sessionTableMutex.Unlock()
for q := range s.Values {
delete(s.Values, q)
}
}
} else {
break
}
}
done <- true
}
// setupAmSession sets up a newly created Amsterdam session.
func setupAmSession(session *sessions.Session) {
u, err := database.AmGetAnonUser()
if err == nil {
session.Values["user_id"] = u.Uid
} else {
log.Errorf("Unable to load anon user: %v", err)
// init registers the time.Time value to be gobbed.
func init() {
gob.Register(time.Time{})
}
// SetupSessionManager sets up the session manager.
func SetupSessionManager() func() {
// create session store
SessionStore = memstore.NewMemStore([]byte(config.GlobalConfig.Rendering.CookieKey))
// create session table
sessionTable = make(map[string]*sessions.Session)
// get the time for the session to expire
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
if err != nil {
d, err = time.ParseDuration("1h")
if err != nil {
panic(err.Error())
}
}
sessionExpiry = d
// get the clock value to run sweeps
d, err = time.ParseDuration("1s")
if err != nil {
panic(err.Error())
}
// set up the sweep runner
sweepRunning.Store(true)
tkr := time.NewTicker(d)
done := make(chan bool)
go sweepSessions(tkr.C, done)
return func() {
// stop the sweep runner
sweepRunning.Store(false)
<-done
tkr.Stop()
}
}
// AmSessionUid returns the current user ID of the session.
func AmSessionUid(session *sessions.Session) int32 {
return session.Values["user_id"].(int32)
}
/* AmSetSessionUser sets the user for the session.
* Parameters:
* session - The session to be updated.
* user - The user to be associated with the session.
*/
func AmSetSessionUser(session *sessions.Session, user *database.User) {
session.Values["user_id"] = user.Uid
session.Values["user_name"] = user.Username
session.Values["user_anon"] = user.IsAnon
}
// setSessionAnon sets the user for the session to the anonymous user.
func setSessionAnon(session *sessions.Session) {
u, err := database.AmGetAnonUser()
if err == nil {
AmSetSessionUser(session, u)
} else {
log.Errorf("unable to set anonymous user: %v", err)
}
}
// AmSessionFirstTime initializes the session after it's first created.
func AmSessionFirstTime(session *sessions.Session) {
key := uuid.NewString()
session.Values["key"] = key
setSessionAnon(session)
sessionTableMutex.Lock()
sessionTable[key] = session
if len(sessionTable) > sessionTableMax {
sessionTableMax = len(sessionTable)
}
session.Values["lasthit"] = time.Now()
sessionTableMutex.Unlock()
}
// AmResetSession clears the specified session.
func AmResetSession(session *sessions.Session) {
key := session.Values["key"]
for k := range session.Values {
delete(session.Values, k)
}
session.Values["key"] = key
setSessionAnon(session)
session.Values["lasthit"] = time.Now()
}
// AmHitSession "hits" a session, updating its "last hit" time.
func AmHitSession(session *sessions.Session) {
session.Values["lasthit"] = time.Now()
}
/* AmSessions returns information about the currently active sessions.
* Returns:
* Number of users active but not logged in
* List of user names currently logged in
* Maximum number of users ever in session table.
*/
func AmSessions() (int, []string, int) {
anons := 0
users := make([]string, 0, len(sessionTable))
sessionTableMutex.RLock()
for _, s := range sessionTable {
if s.Values["user_anon"].(bool) {
anons++
} else {
users = append(users, s.Values["user_name"].(string))
}
}
sessionTableMutex.RUnlock()
slices.Sort(users)
return anons, users, sessionTableMax
}
+25 -5
View File
@@ -7,15 +7,35 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*}
<!-- Users Online -->
{{ sb := .GetScratch("__sidebox") }}
<div class="mb-4">
<div class="bg-blue-600 px-2 py-1 rounded-t">
<h3 class="text-white font-bold text-base">Users Online:</h3>
<h3 class="text-white font-bold text-base">{{ sb.Title }}:</h3>
</div>
<div class="bg-blue-400 px-2 py-2 rounded-b">
<div class="text-black text-sm font-bold mb-2">1 total (max 1)</div>
<div class="flex items-center">
<span class="mr-2">🟣</span>
<span class="text-black text-sm">Not logged in (1)</span>
<div class="space-y-1">
{{ if len(sb.Items) > 0 }}
{{ range sb.Items }}
<div class="flex items-center">
{{ if ! .Flags["nobullet"] }}<span class="mr-2">🟣</span>{{ end }}
{{ if .Link != nil }}
{{ if .Flags["bold"] }}
<span class="font-bold text-sm">{{ .Text }}</span>
{{ else }}
<span class="text-sm">{{ .Text }}</span>
{{ end }}
{{ else }}
{{ if .Flags["bold"] }}
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ .Text }}</a>
{{ else }}
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900 text-sm">{{ .Text }}</a>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">You are not a member of any communities.</div>
{{ end }}
</div>
</div>
</div>