shotgun/serve/session.go

68 lines
1.8 KiB
Go

package serve
import (
"context"
"net/http"
"time"
"git.sunturtle.xyz/studio/shotgun/player"
)
type ctxKey[T any] struct{}
func value[T any](ctx context.Context) T {
r, _ := ctx.Value(ctxKey[T]{}).(T)
return r
}
func with[T any](ctx context.Context, v T) context.Context {
return context.WithValue(ctx, ctxKey[T]{}, v)
}
const sessionCookie = "__Host-id-v1"
// SetSession sets a cookie carrying a session token on a response.
func SetSession(w http.ResponseWriter, s player.Session) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookie,
Value: s.String(),
Path: "/",
Expires: time.Now().Add(365 * 24 * time.Hour),
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
// WithSession is a middleware that adds a player ID to the request context
// based on the session cookie content. If there is no such cookie, or its
// value is invalid, the request fails with a 401 error.
func WithSession(sessions player.RowQuerier) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(sessionCookie)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
id, err := player.ParseSession(c.Value)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
p, err := player.FromSession(r.Context(), sessions, id, time.Now())
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := with(r.Context(), p)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Session returns the session ID set by WithSession in the request context.
func Session(ctx context.Context) player.Session {
return value[player.Session](ctx)
}