package server import ( "context" "net/http" "strings" "time" "git.wntrmute.dev/kyle/metacrypt/internal/auth" "git.wntrmute.dev/kyle/metacrypt/internal/seal" ) type contextKey string const tokenInfoKey contextKey = "tokenInfo" // TokenInfoFromContext extracts the validated token info from the request context. func TokenInfoFromContext(ctx context.Context) *auth.TokenInfo { info, _ := ctx.Value(tokenInfoKey).(*auth.TokenInfo) return info } // loggingMiddleware logs HTTP requests, stripping sensitive headers. func (s *Server) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() sw := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(sw, r) s.logger.Info("http request", "method", r.Method, "path", r.URL.Path, "status", sw.status, "duration", time.Since(start), "remote", r.RemoteAddr, ) }) } // requireUnseal rejects requests unless the service is unsealed. func (s *Server) requireUnseal(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state := s.seal.State() switch state { case seal.StateUninitialized: s.logger.Debug("request rejected: service uninitialized", "path", r.URL.Path) http.Error(w, `{"error":"not initialized"}`, http.StatusPreconditionFailed) return case seal.StateSealed, seal.StateInitializing: s.logger.Debug("request rejected: service sealed", "path", r.URL.Path) http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable) return } next(w, r) } } // requireAuth validates the bearer token and injects TokenInfo into context. func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc { return s.requireUnseal(func(w http.ResponseWriter, r *http.Request) { token := extractToken(r) if token == "" { s.logger.Debug("request rejected: missing token", "path", r.URL.Path) http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } info, err := s.auth.ValidateToken(token) if err != nil { s.logger.Debug("request rejected: invalid token", "path", r.URL.Path, "error", err) http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } s.logger.Debug("request authenticated", "path", r.URL.Path, "username", info.Username) ctx := context.WithValue(r.Context(), tokenInfoKey, info) next(w, r.WithContext(ctx)) }) } // requireAdmin requires the authenticated user to have admin role. func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc { return s.requireAuth(func(w http.ResponseWriter, r *http.Request) { info := TokenInfoFromContext(r.Context()) if info == nil || !info.IsAdmin { s.logger.Debug("request rejected: admin required", "path", r.URL.Path) http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return } s.logger.Debug("admin request authorized", "path", r.URL.Path, "username", info.Username) next(w, r) }) } func extractToken(r *http.Request) string { // Check Authorization header first. authHeader := r.Header.Get("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { return strings.TrimPrefix(authHeader, "Bearer ") } // Fall back to cookie. cookie, err := r.Cookie("metacrypt_token") if err == nil { return cookie.Value } return "" } type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) }