{}

Sessions & auth

BESPA stays out of sessions and authentication — neither lives in the framework. Bring whichever session library you like (scs, gorilla/sessions, or your own), wrap your mux with its middleware, and read the session inside your handlers. This page is the integration pattern.

Wire the middleware

Standard net/http middleware. The session library handles cookies and storage; you treat it as an opaque blob on the request context:

// session.go in your app — wraps a session library you chose.
var sessions = scs.New() // github.com/alexedwards/scs/v2

// In main():
mux := http.NewServeMux()
mux.HandleFunc("/bespa/", widget.AssetRegistry.ServeHTTP)
mux.HandleFunc("/", handleHome)
http.ListenAndServe(":8080", sessions.LoadAndSave(mux))

Middleware order is up to you. Compression, logging, request ID — any of these can wrap or be wrapped by the session middleware; BESPA is unaware. See Handlers & routing for the only middleware-order rule the framework imposes.

Read in a handler

Anywhere inside a handler, read whatever your library exposes off r.Context(). If the user isn’t logged in, redirect to the login page with _back so they return to where they tried to go:

// Inside any handler:
func handleProfile(w http.ResponseWriter, r *http.Request) {
    userID := sessions.GetInt(r.Context(), "user_id")
    if userID == 0 {
        // Send the user to login, remember where they were.
        wf.Redirect(w, r, "/login?_back=" + url.QueryEscape(r.URL.String()))
        return
    }
    // ... render the page with userID in scope ...
}

Gate routes with middleware

Standard net/http middleware composes. Gate one handler or wrap a whole sub-tree:

// Wrap a single handler:
mux.Handle("/admin/", requireLogin(adminMux))

// Or wrap a whole sub-mux:
adminMux := http.NewServeMux()
adminMux.HandleFunc("/admin/users", handleUsers)
adminMux.HandleFunc("/admin/orders", handleOrders)
mux.Handle("/admin/", http.StripPrefix("/admin", requireLogin(adminMux)))

func requireLogin(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if sessions.GetInt(r.Context(), "user_id") == 0 {
            wf.Redirect(w, r, "/login?_back=" + url.QueryEscape(r.URL.String()))
            return
        }
        next.ServeHTTP(w, r)
    })
}

Login form

A BESPA login flow is just a form whose handler reads credentials and stores user state in the session. The RedirectBack helper returns the user to wherever they came from:

func handleLogin(w http.ResponseWriter, r *http.Request) {
    form := wf.Form().Add(
        wf.Field().AddLeft("Email").AddRight(
            wf.InputEmail("email", "").WithRequired(true),
        ),
        wf.Field().AddLeft("Password").AddRight(
            wf.InputPassword("password", "").WithRequired(true),
        ),
        wf.ButtonFilled("login").Add("Sign in"),
    )
    if form.ReadyToCommit(r) {
        user, err := authenticate(form.Values(r).Get("email"), form.Values(r).Get("password"))
        if err == nil {
            sessions.Put(r.Context(), "user_id", user.ID)
            // RedirectBack returns to _back, or to / if unset.
            if !wf.RedirectBack(w, r) {
                wf.Redirect(w, r, "/")
            }
            return
        }
        // Fall through and re-render with a Snackbar holding the message.
    }
    wf.Page().Add(wf.AppBar("Sign in"), form).Draw(w, r)
}

Open it in a modal (?modal=/login) for a soft prompt that doesn’t lose the user’s context, or hit it as a full page for first-time sign-in. Both work with the same handler.

CSRF

Most session libraries include a CSRF helper (nosurf, gorilla/csrf, scs’s Token). Render the token as a hidden field in every state-changing form:

wf.Form().Add(
    wf.InputHidden("_csrf", nosurf.Token(r)),
    // ... fields ...
)

// On the receiving side, your CSRF middleware reads the same token and
// rejects mismatches before the handler runs.

BESPA forms post all hidden inputs along with visible ones, so the token rides back automatically.

OAuth and external redirects

The _back state variable survives an external OAuth round-trip if you embed it in the OAuth state parameter and restore it on the callback. On success, set the session and RedirectBack:

Per-user preferences

Anything that’s per-user and should survive navigation — theme, locale, layout preferences — belongs in the session, not the URL. The website’s own theme/palette switcher uses this pattern; its Render wrapper reads the session and applies the user’s choices before drawing every page:

func Render(w http.ResponseWriter, r *http.Request, page *widget.PageWidget) {
    session := SessionOf(w, r)            // your helper
    switch session.Theme {
    case "Dark":
        page.WithThemeDark()
    case "Light":
        page.WithThemeLight()
    }
    if session.Palette != "" {
        for _, kc := range css.PresetKeyColors {
            if kc.Name == session.Palette {
                page.WithKeyColors(kc)
                break
            }
        }
    }
    page.Draw(w, r)
}

In contrast, anything that’s per-page and should be bookmarkable — search query, sort order, the currently open modal — belongs in the URL. See State patterns for the URL-vs-session rule.

See also

Handlers & routing — where the session middleware sits relative to the rest.

Forms & validation — the form / RedirectBack / persist-error pattern used in the login example.

website/shared/session.go in this repo — a small concrete session helper used by the website example.