This commit is contained in:
Tomas Dvorak
2026-01-26 08:13:18 +01:00
parent aa036b6550
commit dfc079288f
505 changed files with 95755 additions and 5712 deletions
+7 -1
View File
@@ -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()
+181
View File
@@ -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
}
+21 -19
View File
@@ -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