Session
Implement a user session with middleware and cookies.
Session
An example of how to implement a user session using middleware and cookies. It also shows how to pass data from middleware to Components.
Using middleware in HLive is just like any Go app.
Live Demo
Source
package examples
import (
"context"
"errors"
"log"
"net/http"
"sync"
l "github.com/SamHennessy/hlive"
. "github.com/SamHennessy/hlive/hhtml"
"github.com/rs/xid"
)
// SessionDemo serves a live, interactive instance of the Session example for
// the "Live Demo" iframe. It needs its cookie middleware wired around the
// page server, so it returns an http.Handler instead of a plain *l.Page.
func SessionDemo() http.Handler {
s := newService()
return http.HandlerFunc(sessionMiddleware(l.NewPageServer(func() *l.Page { return sessionPage(s) }).ServeHTTP))
}
func sessionPage(s *service) *l.Page {
page := l.NewPage()
page.DOM().Title().Add("HTTP Session Example")
page.DOM().Head().Add(Link(Rel("stylesheet"), Href("https://cdn.simplecss.org/simple.min.css")))
page.DOM().Body().Add(
Header(
H1("HTTP Session"),
P("You can use middleware to implement a persistent session."),
),
Main(
P("Enter a message, then open another tab to see it there."),
H2("Your Message"),
newMessage(s),
P(
"This example uses a cookie and server memory to persist between page reloads but not server reloads. Changes are not synced between tabs in real-time."),
P("Be careful when testing in Firefox, as it will keep the current form value on refresh."),
),
)
return page
}
type sessionCtxKey string
const sessionKey sessionCtxKey = "session"
func sessionMiddleware(h http.HandlerFunc) http.HandlerFunc {
cookieName := "hlive_session"
return func(w http.ResponseWriter, r *http.Request) {
var sessionID string
cook, err := r.Cookie(cookieName)
switch {
case errors.Is(err, http.ErrNoCookie):
sessionID = xid.New().String()
http.SetCookie(w,
&http.Cookie{Name: cookieName, Value: sessionID, Path: "/", SameSite: http.SameSiteStrictMode})
case err != nil:
log.Println("ERROR: get cookie: ", err.Error())
default:
sessionID = cook.Value
}
r = r.WithContext(context.WithValue(r.Context(), sessionKey, sessionID))
h(w, r)
}
}
func getSessionID(ctx context.Context) string {
val, _ := ctx.Value(sessionKey).(string)
return val
}
// This live demo is public and always-on, so the in-memory store is bounded
// and thread-safe: the original example is meant for one local developer at
// a time, and neither guards against concurrent map access nor evicts old
// entries.
const maxSessions = 1000
func newService() *service {
return &service{userMessage: map[string]string{}}
}
type service struct {
mu sync.Mutex
userMessage map[string]string
order []string
}
func (s *service) SetMessage(userID, message string) {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.userMessage[userID]; !exists {
s.order = append(s.order, userID)
if len(s.order) > maxSessions {
var oldest string
oldest, s.order = s.order[0], s.order[1:]
delete(s.userMessage, oldest)
}
}
s.userMessage[userID] = message
}
func (s *service) GetMessage(userID string) string {
s.mu.Lock()
defer s.mu.Unlock()
return s.userMessage[userID]
}
// Forced to a Component (rather than the hhtml Textarea() tag builder)
// because its input binding is attached after creation.
func newMessage(service *service) *message {
c := &message{
Component: l.C("textarea"),
service: service,
}
c.Add(l.On("input", func(ctx context.Context, e l.Event) {
c.service.SetMessage(getSessionID(ctx), e.Value)
}))
return c
}
type message struct {
*l.Component
Message string
service *service
}
func (c *message) Mount(ctx context.Context) {
c.Message = c.service.GetMessage(getSessionID(ctx))
}
func (c *message) GetNodes() *l.NodeGroup {
return l.Group(c.Message)
}