package api import ( "context" "net/http" "geniusrun/backend/internal/auth" ) type userContextKey int const resolvedUserContextKey userContextKey = iota // resolvedUser is the geniusrun account (if any) bound to the current // session's OIDC subject. type resolvedUser struct { ID int64 DisplayName string } // resolveUser runs after auth.RequireSession on every request and looks up // whether the session's OIDC subject has a provisioned geniusrun user. It // never blocks the request itself -- it only attaches the result (found or // not) to context -- since a couple of routes (session/me, setup) must stay // reachable for an authorized-but-not-yet-provisioned session. Routes that // require a provisioned user are wrapped in requireProvisionedUser as well. func (s *Server) resolveUser(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims, ok := auth.ClaimsFromContext(r.Context()) if !ok { // Unreachable in practice -- RequireSession already 401s before // this middleware runs -- but fail closed rather than panic. next.ServeHTTP(w, r) return } u, found, err := s.DB.GetUserBySub(r.Context(), claims.Sub) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } ctx := r.Context() if found { ctx = context.WithValue(ctx, resolvedUserContextKey, resolvedUser{ID: u.ID, DisplayName: u.DisplayName}) } next.ServeHTTP(w, r.WithContext(ctx)) }) } // requireProvisionedUser wraps routes that operate on a user's data: it // 403s if the session's OIDC identity has no provisioned geniusrun user yet // (resolveUser must run earlier in the chain). This -- not any // client-supplied id -- is the only source of truth for "which user's data" // a request may touch. func requireProvisionedUser(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, ok := userFromContext(r.Context()); !ok { writeError(w, http.StatusForbidden, "no profile provisioned for this account yet") return } next.ServeHTTP(w, r) }) } // userFromContext returns the resolved user for the current session, as // populated by resolveUser. func userFromContext(ctx context.Context) (resolvedUser, bool) { u, ok := ctx.Value(resolvedUserContextKey).(resolvedUser) return u, ok } // userIDFromContext is a convenience for the overwhelming majority of // handlers, which only need the id. Panics if called somewhere // requireProvisionedUser didn't already guarantee a resolved user -- that // would be a routing bug, not a runtime condition to handle gracefully. func userIDFromContext(ctx context.Context) int64 { u, ok := userFromContext(ctx) if !ok { panic("api: userIDFromContext called without requireProvisionedUser in the middleware chain") } return u.ID }