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) }