Files
shorebird-server/internal/api/middleware/auth.go
T
Tony 5f72264b06 feat: add organization management features and password reset functionality
- Introduced a new section for managing organizations, including creating organizations and adding users to them.
- Added a password reset modal and functionality to request a password reset link.
- Updated the settings page to include personal settings for changing passwords and admin settings for configuring registration options.
- Enhanced the app detail view with tabs for releases, insights, collaborators, tracks, and settings.
- Improved user management with admin capabilities to verify user emails and change user roles.
- Updated navigation and UI elements to accommodate new features and improve user experience.
2026-06-12 20:03:58 +08:00

72 lines
2.1 KiB
Go

package middleware
import (
"context"
"net/http"
"strings"
"github.com/shorebird-server/internal/auth"
"github.com/shorebird-server/internal/db"
)
type contextKey string
const (
// ClaimsKey is the context key for auth claims.
ClaimsKey contextKey = "auth_claims"
)
// AuthMiddleware validates JWT tokens and injects claims into the request context.
func AuthMiddleware(authService *auth.Service, store db.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"message":"Missing Authorization header"}`, http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
http.Error(w, `{"message":"Invalid Authorization header format"}`, http.StatusUnauthorized)
return
}
token := parts[1]
// Strip sb_api_ prefix (self-hosted API key format for Shorebird CLI compatibility)
token = strings.TrimPrefix(token, "sb_api_")
claims, err := authService.ValidateToken(token)
if err != nil {
http.Error(w, `{"message":"Invalid or expired token"}`, http.StatusUnauthorized)
return
}
user, err := store.GetUserByID(r.Context(), claims.UserID)
if err != nil {
http.Error(w, `{"message":"User not found"}`, http.StatusUnauthorized)
return
}
if !user.EmailVerified {
http.Error(w, `{"message":"Email verification required"}`, http.StatusForbidden)
return
}
if user.MustChangePassword && r.URL.Path != "/api/v1/users/me" && r.URL.Path != "/api/v1/users/me/password" {
http.Error(w, `{"message":"Password change required"}`, http.StatusForbidden)
return
}
ctx := context.WithValue(r.Context(), ClaimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetClaims extracts auth claims from the request context.
func GetClaims(r *http.Request) *auth.Claims {
claims, ok := r.Context().Value(ClaimsKey).(*auth.Claims)
if !ok {
return nil
}
return claims
}