working on session management and the Users Online sidebox (not quite there yet)
This commit is contained in:
@@ -52,6 +52,7 @@ type AmConfig struct {
|
||||
TopRefresh int `yaml:"topRefresh"`
|
||||
LoginCookieName string `yaml:"loginCookieName"`
|
||||
LoginCookieAge int `yaml:"loginCookieAge"`
|
||||
SessionExpire string `yaml:"sessionExpire"`
|
||||
UserAgreement struct {
|
||||
Title string `yaml:"title"`
|
||||
Text string `yaml:"text"`
|
||||
@@ -138,6 +139,7 @@ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) {
|
||||
dest.Site.TopRefresh = overlayInt(loaded.Site.TopRefresh, defaults.Site.TopRefresh)
|
||||
dest.Site.LoginCookieName = overlayString(loaded.Site.LoginCookieName, defaults.Site.LoginCookieName)
|
||||
dest.Site.LoginCookieAge = overlayInt(loaded.Site.LoginCookieAge, defaults.Site.LoginCookieAge)
|
||||
dest.Site.SessionExpire = overlayString(loaded.Site.SessionExpire, defaults.Site.SessionExpire)
|
||||
dest.Site.UserAgreement.Title = overlayString(loaded.Site.UserAgreement.Title, defaults.Site.UserAgreement.Title)
|
||||
dest.Site.UserAgreement.Text = overlayString(loaded.Site.UserAgreement.Text, defaults.Site.UserAgreement.Text)
|
||||
dest.Database.Driver = overlayString(loaded.Database.Driver, defaults.Database.Driver)
|
||||
|
||||
@@ -12,6 +12,7 @@ site:
|
||||
topRefresh: 300
|
||||
loginCookieName: AmsterdamAuth
|
||||
loginCookieAge: 365
|
||||
sessionExpire: "3h"
|
||||
userAgreement:
|
||||
title: "Amsterdam User Agreement"
|
||||
text: >
|
||||
|
||||
@@ -4,8 +4,17 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/CloudyKit/jet/v6 v6.3.1
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/biter777/countries v1.7.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/labstack/echo-contrib v0.17.4
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -13,19 +22,11 @@ require (
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||
github.com/biter777/countries v1.7.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/labstack/echo-contrib v0.17.4
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
|
||||
@@ -18,6 +18,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
|
||||
@@ -68,7 +68,8 @@ func main() {
|
||||
closer = email.SetupMailSender()
|
||||
defer closer()
|
||||
ui.SetupTemplates()
|
||||
ui.SetupSessionManager()
|
||||
closer = ui.SetupSessionManager()
|
||||
defer closer()
|
||||
ui.SetupLeftMenus()
|
||||
|
||||
// Set up Echo.
|
||||
|
||||
@@ -55,8 +55,8 @@ 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
|
||||
out.Items[i].Link = new(string)
|
||||
*out.Items[i].Link = "/TODO/community/" + c.Alias
|
||||
lk := fmt.Sprintf("/TODO/community/%s", c.Alias)
|
||||
out.Items[i].Link = &lk
|
||||
out.Items[i].Flags = make(map[string]bool)
|
||||
var level uint16
|
||||
level, err = database.AmGetCommunityAccessLevel(uid, c.Id)
|
||||
@@ -67,6 +67,7 @@ func buildCommunitiesSidebox(uid int32, out *RenderedSidebox, in *database.Sideb
|
||||
out.TemplateName = "sb_ftrcomm.jet"
|
||||
}
|
||||
}
|
||||
_ = in
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,7 +93,33 @@ func buildFeaturedConferences(uid int32, out *RenderedSidebox, in *database.Side
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func buildUsersOnline(uid int32, out *RenderedSidebox, in *database.Sidebox) error {
|
||||
out.Title = "Users Online"
|
||||
out.TemplateName = "sb_online.jet"
|
||||
anons, users, maxUsers := ui.AmSessions()
|
||||
cap := len(users) + 1
|
||||
if anons > 0 {
|
||||
cap++
|
||||
}
|
||||
out.Items = make([]RenderedSideboxItem, cap)
|
||||
out.Items[0].Text = fmt.Sprintf("%d total (max %d)", len(users)+anons, maxUsers)
|
||||
out.Items[0].Flags = make(map[string]bool)
|
||||
out.Items[0].Flags["nobullet"] = true
|
||||
out.Items[0].Flags["bold"] = true
|
||||
b := 1
|
||||
if anons > 0 {
|
||||
out.Items[1].Text = fmt.Sprintf("Not logged in (%d)", anons)
|
||||
out.Items[1].Flags = make(map[string]bool)
|
||||
b++
|
||||
}
|
||||
for i, n := range users {
|
||||
out.Items[b+i].Text = n
|
||||
lk := fmt.Sprintf("/TODO/user/%s", n)
|
||||
out.Items[b+i].Link = &lk
|
||||
out.Items[b+i].Flags = make(map[string]bool)
|
||||
out.Items[b+i].Flags["bold"] = true
|
||||
}
|
||||
_ = uid
|
||||
_ = in
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user