mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 04:01:34 +00:00
This commit addresses several high-priority tasks from the TASKS.md file, including: - **Fix Background Job Panic:** Replaced `log.Fatalf` with `log.Printf` in the `asynq` server to prevent crashes. - **Refactor API Server Setup:** Consolidated the GraphQL Playground and Prometheus metrics endpoints into the main API server. - **Implement `DeleteUser` Mutation:** Implemented the `DeleteUser` resolver. - **Implement `CreateContribution` Mutation:** Implemented the `CreateContribution` resolver and its required application service. Additionally, this commit includes a major refactoring of the configuration management system to fix a broken build. The global `config.Cfg` variable has been removed and replaced with a dependency injection approach, where the configuration object is passed to all components that require it. This change has been applied across the entire codebase, including the test suite, to ensure a stable and testable application.
195 lines
5.5 KiB
Go
195 lines
5.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"tercul/internal/observability"
|
|
"tercul/internal/platform/log"
|
|
)
|
|
|
|
// ContextKey is a type for context keys
|
|
type ContextKey string
|
|
|
|
const (
|
|
// UserContextKey is the key for user in context
|
|
UserContextKey ContextKey = "user"
|
|
// ClaimsContextKey is the key for claims in context
|
|
ClaimsContextKey ContextKey = "claims"
|
|
)
|
|
|
|
// AuthMiddleware creates middleware for JWT authentication
|
|
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger := log.FromContext(r.Context())
|
|
// Skip authentication for certain paths
|
|
if shouldSkipAuth(r.URL.Path) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Extract token from Authorization header
|
|
authHeader := r.Header.Get("Authorization")
|
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
|
if err != nil {
|
|
logger.Warn("Authentication failed - missing or invalid token")
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Validate token
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err != nil {
|
|
logger.Warn("Authentication failed - invalid token")
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Add claims to context
|
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// RoleMiddleware creates middleware for role-based authorization
|
|
func RoleMiddleware(jwtManager *JWTManager, requiredRole string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger := log.FromContext(r.Context())
|
|
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
|
|
if !ok {
|
|
logger.Warn("Authorization failed - no claims in context")
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
|
logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role")
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// GraphQLAuthMiddleware creates middleware specifically for GraphQL requests
|
|
func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger := log.FromContext(r.Context())
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
|
if err != nil {
|
|
logger.Error(err, "GraphQL authentication failed - could not extract token")
|
|
next.ServeHTTP(w, r) // Proceed without auth
|
|
return
|
|
}
|
|
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err != nil {
|
|
logger.Error(err, "GraphQL authentication failed - invalid token")
|
|
next.ServeHTTP(w, r) // Proceed without auth
|
|
return
|
|
}
|
|
|
|
// Add claims and enriched logger to context for authenticated requests
|
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
|
enrichedLogger := logger.With("user_id", claims.UserID)
|
|
ctx = context.WithValue(ctx, observability.LoggerContextKey, enrichedLogger)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetClaimsFromContext extracts claims from context
|
|
func GetClaimsFromContext(ctx context.Context) (*Claims, bool) {
|
|
claims, ok := ctx.Value(ClaimsContextKey).(*Claims)
|
|
return claims, ok
|
|
}
|
|
|
|
// GetUserIDFromContext extracts user ID from context
|
|
func GetUserIDFromContext(ctx context.Context) (uint, bool) {
|
|
claims, ok := GetClaimsFromContext(ctx)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return claims.UserID, true
|
|
}
|
|
|
|
// IsAuthenticated checks if the request is authenticated
|
|
func IsAuthenticated(ctx context.Context) bool {
|
|
_, ok := GetClaimsFromContext(ctx)
|
|
return ok
|
|
}
|
|
|
|
// RequireAuth ensures the request is authenticated
|
|
func RequireAuth(ctx context.Context) (*Claims, error) {
|
|
claims, ok := GetClaimsFromContext(ctx)
|
|
if !ok {
|
|
return nil, ErrMissingToken
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// RequireRole ensures the user has the required role
|
|
func RequireRole(ctx context.Context, jwtManager *JWTManager, requiredRole string) (*Claims, error) {
|
|
claims, err := RequireAuth(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
// shouldSkipAuth determines if authentication should be skipped for a path
|
|
func shouldSkipAuth(path string) bool {
|
|
skipPaths := []string{
|
|
"/",
|
|
"/query",
|
|
"/health",
|
|
"/metrics",
|
|
"/favicon.ico",
|
|
}
|
|
|
|
for _, skipPath := range skipPaths {
|
|
if path == skipPath {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Skip static files
|
|
if strings.HasPrefix(path, "/static/") {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ContextWithUserID adds a user ID to the context for testing purposes.
|
|
func ContextWithUserID(ctx context.Context, userID uint) context.Context {
|
|
claims := &Claims{UserID: userID}
|
|
return context.WithValue(ctx, ClaimsContextKey, claims)
|
|
}
|
|
|
|
// ContextWithAdminUser adds an admin user to the context for testing purposes.
|
|
func ContextWithAdminUser(ctx context.Context, userID uint) context.Context {
|
|
claims := &Claims{
|
|
UserID: userID,
|
|
Role: "admin",
|
|
}
|
|
return context.WithValue(ctx, ClaimsContextKey, claims)
|
|
}
|