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)) }) } } // ReqPlayer returns the session ID set by WithSession in the request context. func ReqPlayer(ctx context.Context) player.ID { return value[player.ID](ctx) }