mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
hot fix #1
This commit is contained in:
@@ -157,7 +157,13 @@ func RoleAuth(requiredRole string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has the required role
|
||||
// Editors are allowed to access routes that require either "editor" or "admin"
|
||||
if userRole == "editor" && (requiredRole == "editor" || requiredRole == "admin") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has the required role exactly
|
||||
if userRole != requiredRole {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
||||
c.Abort()
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/pkg/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// I18nMiddleware handles internationalization
|
||||
type I18nMiddleware struct {
|
||||
db *gorm.DB
|
||||
defaultLang *models.Language
|
||||
}
|
||||
|
||||
// NewI18nMiddleware creates a new i18n middleware
|
||||
func NewI18nMiddleware() (*I18nMiddleware, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
// Get default language
|
||||
defaultLang, err := models.GetDefaultLanguage(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &I18nMiddleware{
|
||||
db: db,
|
||||
defaultLang: defaultLang,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Middleware returns the Gin middleware function
|
||||
func (m *I18nMiddleware) Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Try to get language from various sources in priority order
|
||||
|
||||
// 1. User preference (if authenticated)
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
if userPref, err := m.getUserLanguage(userID.(uint)); err == nil {
|
||||
c.Set("language", userPref.LanguageCode)
|
||||
c.Set("language_name", userPref.Language.Name)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. URL parameter (?lang=en)
|
||||
if lang := c.Query("lang"); lang != "" && m.isValidLanguage(lang) {
|
||||
c.Set("language", lang)
|
||||
c.Set("language_name", m.getLanguageName(lang))
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Cookie
|
||||
if cookie, err := c.Cookie("lang"); err == nil && m.isValidLanguage(cookie) {
|
||||
c.Set("language", cookie)
|
||||
c.Set("language_name", m.getLanguageName(cookie))
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Accept-Language header
|
||||
if header := c.GetHeader("Accept-Language"); header != "" {
|
||||
if lang := m.parseAcceptLanguage(header); lang != "" {
|
||||
c.Set("language", lang)
|
||||
c.Set("language_name", m.getLanguageName(lang))
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Default language
|
||||
c.Set("language", m.defaultLang.Code)
|
||||
c.Set("language_name", m.defaultLang.Name)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getUserLanguage gets user's preferred language from database
|
||||
func (m *I18nMiddleware) getUserLanguage(userID uint) (*models.UserLanguagePreference, error) {
|
||||
var pref models.UserLanguagePreference
|
||||
err := m.db.Where("user_id = ?", userID).Preload("Language").First(&pref).Error
|
||||
return &pref, err
|
||||
}
|
||||
|
||||
// isValidLanguage checks if language code is valid and active
|
||||
func (m *I18nMiddleware) isValidLanguage(code string) bool {
|
||||
var count int64
|
||||
m.db.Model(&models.Language{}).Where("code = ? AND is_active = ?", code, true).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// getLanguageName returns the name of a language
|
||||
func (m *I18nMiddleware) getLanguageName(code string) string {
|
||||
var lang models.Language
|
||||
err := m.db.Where("code = ?", code).First(&lang).Error
|
||||
if err != nil {
|
||||
return m.defaultLang.Name
|
||||
}
|
||||
return lang.Name
|
||||
}
|
||||
|
||||
// parseAcceptLanguage parses Accept-Language header and returns best match
|
||||
func (m *I18nMiddleware) parseAcceptLanguage(header string) string {
|
||||
// Simple implementation - split by comma and take first
|
||||
// In production, you'd want to parse q-values
|
||||
langs := strings.Split(header, ",")
|
||||
for _, lang := range langs {
|
||||
lang = strings.TrimSpace(strings.Split(lang, ";")[0])
|
||||
// Convert en-US to en, de-DE to de, etc.
|
||||
if len(lang) > 2 {
|
||||
lang = lang[:2]
|
||||
}
|
||||
if m.isValidLanguage(lang) {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetLanguage returns the current language code from context
|
||||
func GetLanguage(c *gin.Context) string {
|
||||
if lang, exists := c.Get("language"); exists {
|
||||
return lang.(string)
|
||||
}
|
||||
return "cs" // fallback to Czech
|
||||
}
|
||||
|
||||
// GetLanguageName returns the current language name from context
|
||||
func GetLanguageName(c *gin.Context) string {
|
||||
if name, exists := c.Get("language_name"); exists {
|
||||
return name.(string)
|
||||
}
|
||||
return "Čeština"
|
||||
}
|
||||
|
||||
// SetLanguageCookie sets the language preference in a cookie
|
||||
func SetLanguageCookie(c *gin.Context, languageCode string) {
|
||||
c.SetCookie("lang", languageCode, 365*24*60*60, "/", "", false, true)
|
||||
}
|
||||
|
||||
// TranslationHelper provides translation functions
|
||||
type TranslationHelper struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTranslationHelper creates a new translation helper
|
||||
func NewTranslationHelper() *TranslationHelper {
|
||||
return &TranslationHelper{
|
||||
db: database.GetDB(),
|
||||
}
|
||||
}
|
||||
|
||||
// T translates a key using the current language context
|
||||
func (th *TranslationHelper) T(c *gin.Context, key string) string {
|
||||
lang := GetLanguage(c)
|
||||
|
||||
translation, err := models.GetTranslationWithFallback(th.db, key, lang)
|
||||
if err != nil {
|
||||
// Return key if translation not found
|
||||
return key
|
||||
}
|
||||
|
||||
return translation.Value
|
||||
}
|
||||
|
||||
// TWithParams translates a key with parameters
|
||||
func (th *TranslationHelper) TWithParams(c *gin.Context, key string, params map[string]interface{}) string {
|
||||
result := th.T(c, key)
|
||||
|
||||
// Simple parameter replacement
|
||||
for k, v := range params {
|
||||
result = strings.ReplaceAll(result, "{{"+k+"}}", v.(string))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"fotbal-club/pkg/logger"
|
||||
"fotbal-club/internal/services"
|
||||
"fotbal-club/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -18,27 +18,28 @@ func CustomRecovery() gin.HandlerFunc {
|
||||
if err := recover(); err != nil {
|
||||
// Get stack trace
|
||||
stack := string(debug.Stack())
|
||||
|
||||
|
||||
// Log the panic
|
||||
requestID := GetRequestID(c)
|
||||
logger.Error("Panic recovered",
|
||||
"request_id", requestID,
|
||||
"error", fmt.Sprintf("%v", err),
|
||||
"stack", stack,
|
||||
"path", c.Request.URL.Path,
|
||||
"method", c.Request.Method,
|
||||
)
|
||||
|
||||
logger.Error(fmt.Sprintf(
|
||||
"Panic recovered: request_id=%s error=%v path=%s method=%s stack=%s",
|
||||
requestID,
|
||||
err,
|
||||
c.Request.URL.Path,
|
||||
c.Request.Method,
|
||||
stack,
|
||||
))
|
||||
|
||||
// Return error response
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal server error",
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -52,13 +53,14 @@ func CustomRecoveryWithReporter(reporter *services.ErrorReporter) gin.HandlerFun
|
||||
if err := recover(); err != nil {
|
||||
stack := string(debug.Stack())
|
||||
requestID := GetRequestID(c)
|
||||
logger.Error("Panic recovered",
|
||||
"request_id", requestID,
|
||||
"error", fmt.Sprintf("%v", err),
|
||||
"stack", stack,
|
||||
"path", c.Request.URL.Path,
|
||||
"method", c.Request.Method,
|
||||
)
|
||||
logger.Error(fmt.Sprintf(
|
||||
"Panic recovered: request_id=%s error=%v path=%s method=%s stack=%s",
|
||||
requestID,
|
||||
err,
|
||||
c.Request.URL.Path,
|
||||
c.Request.Method,
|
||||
stack,
|
||||
))
|
||||
reporter.Report(c.Request.Context(), &services.ErrorEvent{
|
||||
Origin: "backend",
|
||||
Language: "go",
|
||||
|
||||
@@ -45,6 +45,10 @@ func ValidateContentType() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if strings.Contains(path, "/admin/manual/matches/import") || strings.Contains(path, "/admin/manual/tables/import") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Allow scoreboard timer control endpoints without requiring JSON body
|
||||
// These actions do not read request body and are triggered via simple POSTs from remote UI
|
||||
@@ -57,6 +61,11 @@ func ValidateContentType() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "/admin/prefetch/trigger") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "/rembg/start") {
|
||||
c.Next()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user