mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
- Core Go application with GraphQL API using gqlgen - Comprehensive data models for literary works, authors, translations - Repository pattern with caching layer - Authentication and authorization system - Linguistics analysis capabilities with multiple adapters - Vector search integration with Weaviate - Docker containerization support - Python data migration and analysis scripts - Clean architecture with proper separation of concerns - Production-ready configuration and middleware - Proper .gitignore excluding vendor/, database files, and build artifacts
184 lines
5.0 KiB
Go
184 lines
5.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"tercul/logger"
|
|
)
|
|
|
|
// 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) {
|
|
// 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.LogWarn("Authentication failed - missing or invalid token",
|
|
logger.F("path", r.URL.Path),
|
|
logger.F("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Validate token
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err != nil {
|
|
logger.LogWarn("Authentication failed - invalid token",
|
|
logger.F("path", r.URL.Path),
|
|
logger.F("error", err))
|
|
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(requiredRole string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
|
|
if !ok {
|
|
logger.LogWarn("Authorization failed - no claims in context",
|
|
logger.F("path", r.URL.Path),
|
|
logger.F("required_role", requiredRole))
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
jwtManager := NewJWTManager()
|
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
|
logger.LogWarn("Authorization failed - insufficient role",
|
|
logger.F("path", r.URL.Path),
|
|
logger.F("user_role", claims.Role),
|
|
logger.F("required_role", requiredRole))
|
|
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) {
|
|
// For GraphQL, we want to authenticate but not block requests
|
|
// This allows for both authenticated and anonymous queries
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader != "" {
|
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
|
if err == nil {
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err == nil {
|
|
// Add claims to context for authenticated requests
|
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
}
|
|
// If token is invalid, log warning but continue
|
|
logger.LogWarn("GraphQL authentication failed - continuing with anonymous access",
|
|
logger.F("path", r.URL.Path))
|
|
}
|
|
|
|
// Continue without authentication
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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, requiredRole string) (*Claims, error) {
|
|
claims, err := RequireAuth(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jwtManager := NewJWTManager()
|
|
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
|
|
}
|