initial commit

This commit is contained in:
2025-11-29 00:28:21 +05:00
parent 46229acc82
commit ec3b03a935
76 changed files with 13492 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
package middleware
import (
"context"
"encoding/json"
"net/http"
"strings"
"git.kirlllll.ru/volontery/backend/internal/pkg/jwt"
)
type contextKey string
const (
UserIDKey contextKey = "user_id"
UserEmailKey contextKey = "user_email"
)
// ErrorResponse represents a JSON error response
type ErrorResponse struct {
Error string `json:"error"`
}
// JSONError sends a JSON error response
func JSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
// AuthMiddleware создает middleware для проверки JWT токена
func AuthMiddleware(jwtManager *jwt.Manager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Получение токена из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
JSONError(w, "authorization header required", http.StatusUnauthorized)
return
}
// Проверка формата "Bearer <token>"
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
JSONError(w, "invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// Валидация токена
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
JSONError(w, "invalid or expired token", http.StatusUnauthorized)
return
}
// Добавление данных пользователя в контекст
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetUserIDFromContext извлекает ID пользователя из контекста
func GetUserIDFromContext(ctx context.Context) (int64, bool) {
userID, ok := ctx.Value(UserIDKey).(int64)
return userID, ok
}
// GetUserEmailFromContext извлекает email пользователя из контекста
func GetUserEmailFromContext(ctx context.Context) (string, bool) {
email, ok := ctx.Value(UserEmailKey).(string)
return email, ok
}
// OptionalAuthMiddleware делает аутентификацию опциональной (не возвращает ошибку если токена нет)
func OptionalAuthMiddleware(jwtManager *jwt.Manager) 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 == "" {
next.ServeHTTP(w, r)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
next.ServeHTTP(w, r)
return
}
claims, err := jwtManager.ValidateToken(parts[1])
if err != nil {
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -0,0 +1,94 @@
package middleware
import (
"log"
"net/http"
"runtime/debug"
"time"
)
// Logger middleware для логирования HTTP запросов
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Обертка для захвата статус кода
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf(
"%s %s %d %s",
r.Method,
r.RequestURI,
wrapped.statusCode,
time.Since(start),
)
})
}
// responseWriter обертка для захвата статус кода
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Recovery middleware для восстановления после паник
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS middleware для настройки CORS заголовков
func CORS(allowedOrigins []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Проверка разрешенных источников
allowed := false
for _, allowedOrigin := range allowedOrigins {
if allowedOrigin == "*" || allowedOrigin == origin {
allowed = true
break
}
}
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// Обработка preflight запросов
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// ContentTypeJSON устанавливает Content-Type: application/json
func ContentTypeJSON(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,106 @@
package middleware
import (
"net/http"
"git.kirlllll.ru/volontery/backend/internal/service"
)
// RequirePermission создает middleware для проверки разрешения
func RequirePermission(userService *service.UserService, permission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
hasPermission, err := userService.HasPermission(r.Context(), userID, permission)
if err != nil {
http.Error(w, "failed to check permissions", http.StatusInternalServerError)
return
}
if !hasPermission {
http.Error(w, "forbidden: insufficient permissions", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireRole создает middleware для проверки роли
func RequireRole(userService *service.UserService, roleName string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
roles, err := userService.GetUserRoles(r.Context(), userID)
if err != nil {
http.Error(w, "failed to check roles", http.StatusInternalServerError)
return
}
hasRole := false
for _, role := range roles {
if role.Name == roleName {
hasRole = true
break
}
}
if !hasRole {
http.Error(w, "forbidden: required role not assigned", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireAnyRole создает middleware для проверки наличия хотя бы одной из ролей
func RequireAnyRole(userService *service.UserService, roleNames ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
roles, err := userService.GetUserRoles(r.Context(), userID)
if err != nil {
http.Error(w, "failed to check roles", http.StatusInternalServerError)
return
}
hasAnyRole := false
for _, role := range roles {
for _, requiredRole := range roleNames {
if role.Name == requiredRole {
hasAnyRole = true
break
}
}
if hasAnyRole {
break
}
}
if !hasAnyRole {
http.Error(w, "forbidden: none of the required roles assigned", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}