mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/models"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// DB is the global database connection pool
|
||||
DB *gorm.DB
|
||||
|
||||
// dbLogger is the logger instance for database operations
|
||||
dbLogger = logger.New(
|
||||
log.New(log.Writer(), "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second,
|
||||
LogLevel: logger.Warn,
|
||||
Colorful: true,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// InitDB initializes the database connection with connection pooling
|
||||
func InitDB() (*gorm.DB, error) {
|
||||
var err error
|
||||
|
||||
// Set up database connection with connection pooling
|
||||
DB, err = gorm.Open(postgres.Open(config.AppConfig.DatabaseURL), &gorm.Config{
|
||||
Logger: dbLogger,
|
||||
PrepareStmt: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the underlying sql.DB instance to configure connection pool
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set connection pool parameters
|
||||
sqlDB.SetMaxIdleConns(config.AppConfig.MaxIdleConnections)
|
||||
sqlDB.SetMaxOpenConns(config.AppConfig.MaxOpenConnections)
|
||||
sqlDB.SetConnMaxLifetime(config.AppConfig.ConnMaxLifetime)
|
||||
|
||||
log.Println("Database connection established with connection pooling")
|
||||
return DB, nil
|
||||
}
|
||||
|
||||
// GetDB returns the database instance
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
|
||||
// CloseDB closes the database connection
|
||||
func CloseDB() error {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
log.Printf("Error getting database instance: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
log.Printf("Error closing database connection: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Database connection closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithTransaction executes a function within a database transaction
|
||||
func WithTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
panic(r) // re-throw panic after Rollback
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
if rbErr := tx.Rollback().Error; rbErr != nil {
|
||||
return rbErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// MigrateDB runs database migrations for all models
|
||||
func MigrateDB(db *gorm.DB) error {
|
||||
// Enable UUID extension
|
||||
err := db.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run migrations for all models
|
||||
return db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Article{},
|
||||
&models.Category{},
|
||||
&models.Team{},
|
||||
&models.Player{},
|
||||
&models.Sponsor{},
|
||||
&models.Settings{},
|
||||
&models.MatchOverride{},
|
||||
&models.TeamLogoOverride{},
|
||||
&models.ContactMessage{},
|
||||
&models.ContactCategory{},
|
||||
&models.Contact{},
|
||||
&models.NewsletterSubscription{},
|
||||
&models.VisitorEvent{},
|
||||
&models.ArticleTeamLink{},
|
||||
&models.ArticleMatchLink{},
|
||||
&models.CompetitionAlias{},
|
||||
&models.EmailLog{},
|
||||
&models.EmailEvent{},
|
||||
&models.PasswordReset{},
|
||||
&models.AboutPage{},
|
||||
// Add event tables so public endpoints don't fail before any writes occur
|
||||
&models.Event{},
|
||||
&models.EventAttachment{},
|
||||
&models.UploadedFile{},
|
||||
&models.FileUsage{},
|
||||
)
|
||||
}
|
||||
|
||||
// SeedDB populates the database with initial data
|
||||
func SeedDB(db *gorm.DB) error {
|
||||
// Check if we already have data
|
||||
var count int64
|
||||
if err := db.Model(&models.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we already have data, don't seed
|
||||
if count > 0 {
|
||||
log.Println("Database already seeded, skipping...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUser := models.User{
|
||||
Email: "admin@example.com",
|
||||
Password: string(hashedPassword),
|
||||
FirstName: "Admin",
|
||||
LastName: "User",
|
||||
Role: "admin",
|
||||
}
|
||||
|
||||
if err := db.Create(&adminUser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Database seeded successfully with admin user")
|
||||
log.Printf("Admin credentials: admin@example.com / admin123")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck performs a simple database query to check if the connection is alive
|
||||
func HealthCheck() error {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sqlDB.Ping()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Log levels
|
||||
const (
|
||||
LevelDebug = "DEBUG"
|
||||
LevelInfo = "INFO"
|
||||
LevelWarn = "WARN"
|
||||
LevelError = "ERROR"
|
||||
)
|
||||
|
||||
// Logger is the interface for logging
|
||||
var (
|
||||
// DefaultLogger is the default logger instance
|
||||
DefaultLogger = NewLogger(LevelInfo)
|
||||
)
|
||||
|
||||
// Logger represents a simple logger
|
||||
type Logger struct {
|
||||
level string
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger instance with the specified log level
|
||||
func NewLogger(level string) *Logger {
|
||||
return &Logger{level: level}
|
||||
}
|
||||
|
||||
// SetLevel changes the logger level at runtime
|
||||
func (l *Logger) SetLevel(level string) {
|
||||
switch strings.ToUpper(strings.TrimSpace(level)) {
|
||||
case LevelDebug, LevelInfo, LevelWarn, LevelError:
|
||||
l.level = strings.ToUpper(strings.TrimSpace(level))
|
||||
default:
|
||||
// keep previous
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel changes the default logger level at runtime
|
||||
func SetLevel(level string) {
|
||||
if DefaultLogger != nil {
|
||||
DefaultLogger.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func (l *Logger) Debug(message string, args ...interface{}) {
|
||||
if l.shouldLog(LevelDebug) {
|
||||
l.log(LevelDebug, message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func (l *Logger) Info(message string, args ...interface{}) {
|
||||
if l.shouldLog(LevelInfo) {
|
||||
l.log(LevelInfo, message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func (l *Logger) Warn(message string, args ...interface{}) {
|
||||
if l.shouldLog(LevelWarn) {
|
||||
l.log(LevelWarn, message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func (l *Logger) Error(message string, args ...interface{}) {
|
||||
if l.shouldLog(LevelError) {
|
||||
l.log(LevelError, message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldLog checks if the log level is enabled
|
||||
func (l *Logger) shouldLog(level string) bool {
|
||||
levels := map[string]int{
|
||||
LevelDebug: 0,
|
||||
LevelInfo: 1,
|
||||
LevelWarn: 2,
|
||||
LevelError: 3,
|
||||
}
|
||||
|
||||
return levels[level] >= levels[l.level]
|
||||
}
|
||||
|
||||
// log writes the log message to stderr
|
||||
func (l *Logger) log(level, message string, args ...interface{}) {
|
||||
msg := message
|
||||
if len(args) > 0 {
|
||||
msg = fmt.Sprintf(message, args...)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[%s] %s\n", level, msg)
|
||||
}
|
||||
|
||||
// Package level functions
|
||||
|
||||
// Debug logs a debug message using the default logger
|
||||
func Debug(message string, args ...interface{}) {
|
||||
DefaultLogger.Debug(message, args...)
|
||||
}
|
||||
|
||||
// Info logs an info message using the default logger
|
||||
func Info(message string, args ...interface{}) {
|
||||
DefaultLogger.Info(message, args...)
|
||||
}
|
||||
|
||||
// Warn logs a warning message using the default logger
|
||||
func Warn(message string, args ...interface{}) {
|
||||
DefaultLogger.Warn(message, args...)
|
||||
}
|
||||
|
||||
// Error logs an error message using the default logger
|
||||
func Error(message string, args ...interface{}) {
|
||||
DefaultLogger.Error(message, args...)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTClaims represents the JWT claims structure
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateJWT generates a new JWT token for the given user
|
||||
func GenerateJWT(userID uint, email, role string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &JWTClaims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "fotbal-club",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(GetJWTSecret()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ParseJWT parses and validates a JWT token
|
||||
func ParseJWT(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(GetJWTSecret()), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// GetJWTSecret returns the JWT secret from environment variable or a default value
|
||||
func GetJWTSecret() string {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
return "default-secret-key-change-in-production"
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
// GetUserFromContext gets the authenticated user from Gin context
|
||||
func GetUserFromContext(c *gin.Context) (*JWTClaims, error) {
|
||||
// Preferred: claims set by auth middleware
|
||||
if v, ok := c.Get("claims"); ok {
|
||||
if cl, ok2 := v.(*JWTClaims); ok2 {
|
||||
return cl, nil
|
||||
}
|
||||
}
|
||||
// Fallback: parse Authorization header (Bearer <token>)
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil, errors.New("authorization header missing")
|
||||
}
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, errors.New("invalid authorization header format")
|
||||
}
|
||||
token := parts[1]
|
||||
cl, err := ParseJWT(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cl, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// HashPassword hashes a password using bcrypt
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies a password against a hash
|
||||
func CheckPassword(password, hashedPassword string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeHTML removes potentially dangerous HTML tags and attributes
|
||||
// This is a basic implementation. For production, consider using bluemonday library
|
||||
func SanitizeHTML(html string) string {
|
||||
// Remove script tags and content
|
||||
reScript := regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`)
|
||||
html = reScript.ReplaceAllString(html, "")
|
||||
|
||||
// Remove inline event handlers (onclick, onerror, etc.)
|
||||
reEvents := regexp.MustCompile(`(?i)\s*on\w+\s*=\s*["'][^"']*["']`)
|
||||
html = reEvents.ReplaceAllString(html, "")
|
||||
|
||||
// Remove javascript: URLs
|
||||
reJSURL := regexp.MustCompile(`(?i)javascript:`)
|
||||
html = reJSURL.ReplaceAllString(html, "")
|
||||
|
||||
// Remove iframe tags (can be optionally allowed if needed)
|
||||
reIframe := regexp.MustCompile(`(?i)<iframe[^>]*>.*?</iframe>`)
|
||||
html = reIframe.ReplaceAllString(html, "")
|
||||
|
||||
// Remove object/embed tags
|
||||
reObject := regexp.MustCompile(`(?i)<(object|embed)[^>]*>.*?</\1>`)
|
||||
html = reObject.ReplaceAllString(html, "")
|
||||
|
||||
// Remove style tags (if CSS injection is a concern)
|
||||
// Uncomment if you want to remove inline styles
|
||||
// reStyle := regexp.MustCompile(`(?i)<style[^>]*>.*?</style>`)
|
||||
// html = reStyle.ReplaceAllString(html, "")
|
||||
|
||||
return strings.TrimSpace(html)
|
||||
}
|
||||
|
||||
// SanitizeString removes HTML tags entirely and returns plain text
|
||||
func SanitizeString(input string) string {
|
||||
// Remove all HTML tags
|
||||
reHTML := regexp.MustCompile(`<[^>]*>`)
|
||||
text := reHTML.ReplaceAllString(input, " ")
|
||||
|
||||
// Normalize whitespace
|
||||
text = strings.Join(strings.Fields(text), " ")
|
||||
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// ValidateURL checks if a URL is safe (http/https only)
|
||||
func ValidateURL(url string) bool {
|
||||
if url == "" {
|
||||
return true
|
||||
}
|
||||
lower := strings.ToLower(strings.TrimSpace(url))
|
||||
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || strings.HasPrefix(lower, "/")
|
||||
}
|
||||
|
||||
// SanitizeFilename removes dangerous characters from filenames
|
||||
func SanitizeFilename(filename string) string {
|
||||
// Remove path traversal attempts
|
||||
filename = strings.ReplaceAll(filename, "..", "")
|
||||
filename = strings.ReplaceAll(filename, "/", "")
|
||||
filename = strings.ReplaceAll(filename, "\\", "")
|
||||
|
||||
// Allow only safe characters
|
||||
re := regexp.MustCompile(`[^a-zA-Z0-9._-]`)
|
||||
filename = re.ReplaceAllString(filename, "_")
|
||||
|
||||
// Limit length
|
||||
if len(filename) > 200 {
|
||||
filename = filename[:200]
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
// RemoveNullBytes removes null bytes that can cause issues
|
||||
func RemoveNullBytes(s string) string {
|
||||
return strings.ReplaceAll(s, "\x00", "")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type SubscriberTokenClaims struct {
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateSubscriberToken creates a signed token for a subscriber email, expires in `ttl` minutes
|
||||
func GenerateSubscriberToken(email string, ttlMinutes int) (string, error) {
|
||||
if ttlMinutes <= 0 {
|
||||
ttlMinutes = 60
|
||||
}
|
||||
now := time.Now()
|
||||
claims := &SubscriberTokenClaims{
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(ttlMinutes) * time.Minute)),
|
||||
Issuer: "fotbal-club",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(getJWTSecret()))
|
||||
}
|
||||
|
||||
// ParseSubscriberToken validates and returns the email embedded in the token
|
||||
func ParseSubscriberToken(tokenString string) (string, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &SubscriberTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(getJWTSecret()), nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if claims, ok := token.Claims.(*SubscriberTokenClaims); ok && token.Valid {
|
||||
return claims.Email, nil
|
||||
}
|
||||
return "", jwt.ErrTokenMalformed
|
||||
}
|
||||
|
||||
func getJWTSecret() string {
|
||||
s := os.Getenv("JWT_SECRET")
|
||||
if s == "" {
|
||||
return "default-secret-key-change-in-production"
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user