Commit a8f03535 by Augusto

first Commit

parents
root = "."
[build]
pre_cmd = ["swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency --quiet"]
cmd = "go build -o ./bin/server ./cmd/server"
bin = "./bin/server"
include_ext = ["go"]
exclude_dir = ["vendor", "tmp", "docs"]
exclude_regex = ["_test.go"]
[log]
time = true
[color]
main = "magenta"
.env
.env.*
# Air
/tmp
.air.toml.tmp
# Go build artifacts
/bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# Go workspace
/go.work
/go.work.sum
# IDE
.vscode/
.idea/
*.swp
.DS_Store
# Logs
*.log
# Docker
**/.docker/
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/server ./cmd/server
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /out/server /app/server
EXPOSE 3003
CMD ["/app/server"]
package main
import (
"log"
"os"
"github.com/joho/godotenv"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/database"
"ScoutSystemElite/internal/router"
)
// @title Scout System Elite API
// @version 1.0
// @description Football Scouting Platform API - Backend for Scout System Elite
// @host localhost:3000
// @BasePath /api
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
// @securityDefinitions.basic BasicAuth
func main() {
if err := godotenv.Load(".env"); err != nil {
log.Printf("warning: could not load .env file: %v", err)
}
cfg := config.Load()
db, err := database.Connect(cfg)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
r := router.New(db, cfg)
port := cfg.Port
if port == "" {
port = os.Getenv("PORT")
}
if port == "" {
port = "3000"
}
if err := r.Run(":" + port); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
module ScoutSystemElite
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
github.com/pquerna/otp v1.5.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.46.0
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.2 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.29.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
This diff is collapsed. Click to expand it.
package config
import "os"
type Config struct {
Port string
DatabaseURL string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
SSLMode string
JWTSecret string
JWTAccessTokenTTLMinutes string
ProviderUser string
ProviderSecret string
}
func Load() Config {
return Config{
Port: os.Getenv("APP_PORT"),
DatabaseURL: os.Getenv("DATABASE_URL"),
DBHost: os.Getenv("DB_HOST"),
DBPort: os.Getenv("DB_PORT"),
DBUser: os.Getenv("DB_USER"),
DBPassword: os.Getenv("DB_PASSWORD"),
DBName: os.Getenv("DB_NAME"),
SSLMode: envOrDefault("DB_SSLMODE", "disable"),
JWTSecret: os.Getenv("JWT_SECRET"),
JWTAccessTokenTTLMinutes: envOrDefault("JWT_ACCESS_TOKEN_TTL_MINUTES", "1440"),
ProviderUser: os.Getenv("ProviderUser"),
ProviderSecret: os.Getenv("ProviderSecret"),
}
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
package database
import (
"fmt"
"reflect"
"strings"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/models"
)
const nanoidAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
func registerNanoIDHook(db *gorm.DB) {
db.Callback().Create().Before("gorm:create").Register("sselite:nanoid_pk", func(tx *gorm.DB) {
stmt := tx.Statement
if stmt == nil || stmt.Schema == nil {
return
}
pk := stmt.Schema.PrioritizedPrimaryField
if pk == nil || pk.FieldType == nil || pk.FieldType.Kind() != reflect.String {
return
}
// Only for empty string PKs.
current, isZero := pk.ValueOf(tx.Statement.Context, stmt.ReflectValue)
if !isZero {
// ValueOf's isZero already handles "".
_ = current
return
}
id, err := gonanoid.Generate(nanoidAlphabet, 15)
if err != nil {
_ = tx.AddError(err)
return
}
_ = pk.Set(tx.Statement.Context, stmt.ReflectValue, id)
})
}
func ensureEnumType(db *gorm.DB, typeName string, values []string) error {
// Uses Postgres duplicate_object exception to make this idempotent.
quoted := make([]string, 0, len(values))
for _, v := range values {
v = strings.ReplaceAll(v, "'", "''")
quoted = append(quoted, "'"+v+"'")
}
sql := fmt.Sprintf(
"DO $$ BEGIN CREATE TYPE %s AS ENUM (%s); EXCEPTION WHEN duplicate_object THEN null; END $$;",
typeName,
strings.Join(quoted, ","),
)
return db.Exec(sql).Error
}
func Connect(cfg config.Config) (*gorm.DB, error) {
dsn := cfg.DatabaseURL
if dsn == "" {
dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.DBHost,
cfg.DBPort,
cfg.DBUser,
cfg.DBPassword,
cfg.DBName,
cfg.SSLMode,
)
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
registerNanoIDHook(db)
// Ensure enum types exist before AutoMigrate creates columns that reference them.
if err := ensureEnumType(db, "foot", []string{"left", "right", "both"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "gender", []string{"male", "female", "other"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "status", []string{"active", "inactive"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "coach_position", []string{"head_coach", "assistant_coach", "analyst"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "api_sync_status", []string{"pending", "synced", "error"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "report_status", []string{"saved", "finished"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "calendar_event_type", []string{"match", "travel", "player_observation", "meeting", "training", "other"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "list_type", []string{"shortlist", "shadow_team", "target_list"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "position_category", []string{"Forward", "Goalkeeper", "Defender", "Midfield"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "audit_log_action", []string{"create", "update", "delete", "soft_delete", "restore"}); err != nil {
return nil, err
}
if err := db.AutoMigrate(
&models.User{},
&models.UserSession{},
&models.Area{},
&models.Position{},
&models.Player{},
&models.Coach{},
&models.Agent{},
&models.PlayerAgent{},
&models.CoachAgent{},
&models.Report{},
&models.CalendarEvent{},
&models.List{},
&models.ListShare{},
&models.File{},
&models.Category{},
&models.GlobalSetting{},
&models.UserSetting{},
&models.ClientSubscription{},
&models.ClientModule{},
&models.PlayerFeatureCategory{},
&models.PlayerFeatureType{},
&models.PlayerFeatureRating{},
&models.PlayerFeatureSelection{},
&models.ProfileDescription{},
&models.ProfileLink{},
&models.AuditLog{},
); err != nil {
return nil, err
}
return db, nil
}
package errors
import "errors"
type Code string
const (
CodeNotFound Code = "NOT_FOUND"
CodeInvalidInput Code = "INVALID_INPUT"
CodeConflict Code = "CONFLICT"
CodeInternal Code = "INTERNAL"
)
type AppError struct {
Code Code
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func Wrap(code Code, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
func New(code Code, msg string) *AppError {
return &AppError{Code: code, Message: msg}
}
func IsCode(err error, code Code) bool {
var ae *AppError
if errors.As(err, &ae) {
return ae.Code == code
}
return false
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type AgentHandler struct {
service *services.AgentService
}
func NewAgentHandler(db *gorm.DB) *AgentHandler {
return &AgentHandler{
service: services.NewAgentService(db),
}
}
// Create godoc
// @Summary Create a new agent
// @Description Create a new agent
// @Tags Agents
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateAgentRequest true "Agent data"
// @Success 201 {object} services.AgentResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /agents [post]
func (h *AgentHandler) Create(c *gin.Context) {
var req services.CreateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
agent, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, agent)
}
// GetByID godoc
// @Summary Get agent by ID
// @Description Get a single agent by their ID
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Success 200 {object} services.AgentResponse
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [get]
func (h *AgentHandler) GetByID(c *gin.Context) {
id := c.Param("agentId")
agent, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, agent)
}
// FindAll godoc
// @Summary Get all agents
// @Description Get paginated list of agents with optional filters
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param type query string false "Filter by type"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedAgentsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /agents [get]
func (h *AgentHandler) FindAll(c *gin.Context) {
var query services.QueryAgentsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update an agent
// @Description Update an existing agent's information
// @Tags Agents
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param request body services.UpdateAgentRequest true "Agent data"
// @Success 200 {object} services.AgentResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [patch]
func (h *AgentHandler) Update(c *gin.Context) {
id := c.Param("agentId")
var req services.UpdateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
agent, err := h.service.Update(id, req)
if err != nil {
if err.Error() == "agent not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, agent)
}
// Delete godoc
// @Summary Delete an agent
// @Description Soft delete an agent
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [delete]
func (h *AgentHandler) Delete(c *gin.Context) {
id := c.Param("agentId")
if err := h.service.Delete(id); err != nil {
if err.Error() == "agent not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// AssociateWithPlayer godoc
// @Summary Associate agent with player
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param playerId path string true "Player ID"
// @Success 201 {object} services.AgentPlayerAssociationResponse
// @Router /agents/{agentId}/players/{playerId} [post]
func (h *AgentHandler) AssociateWithPlayer(c *gin.Context) {
agentID := c.Param("agentId")
playerID := c.Param("playerId")
res, err := h.service.AssociateWithPlayer(agentID, playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, res)
}
// AssociateWithCoach godoc
// @Summary Associate agent with coach
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param coachId path string true "Coach ID"
// @Success 201 {object} services.AgentCoachAssociationResponse
// @Router /agents/{agentId}/coaches/{coachId} [post]
func (h *AgentHandler) AssociateWithCoach(c *gin.Context) {
agentID := c.Param("agentId")
coachID := c.Param("coachId")
res, err := h.service.AssociateWithCoach(agentID, coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, res)
}
// RemoveFromPlayer godoc
// @Summary Remove agent from player
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Param playerId path string true "Player ID"
// @Success 204
// @Router /agents/{agentId}/players/{playerId} [delete]
func (h *AgentHandler) RemoveFromPlayer(c *gin.Context) {
agentID := c.Param("agentId")
playerID := c.Param("playerId")
if err := h.service.RemoveFromPlayer(agentID, playerID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// RemoveFromCoach godoc
// @Summary Remove agent from coach
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Param coachId path string true "Coach ID"
// @Success 204
// @Router /agents/{agentId}/coaches/{coachId} [delete]
func (h *AgentHandler) RemoveFromCoach(c *gin.Context) {
agentID := c.Param("agentId")
coachID := c.Param("coachId")
if err := h.service.RemoveFromCoach(agentID, coachID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// GetByPlayer godoc
// @Summary Get agents associated with a player
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Success 200 {array} services.AgentResponse
// @Router /agents/by-player/{playerId} [get]
func (h *AgentHandler) GetByPlayer(c *gin.Context) {
playerID := c.Param("playerId")
res, err := h.service.GetAgentsByPlayer(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
// GetByCoach godoc
// @Summary Get agents associated with a coach
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param coachId path string true "Coach ID"
// @Success 200 {array} services.AgentResponse
// @Router /agents/by-coach/{coachId} [get]
func (h *AgentHandler) GetByCoach(c *gin.Context) {
coachID := c.Param("coachId")
res, err := h.service.GetAgentsByCoach(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type AreaHandler struct {
service *services.AreaService
}
func NewAreaHandler(db *gorm.DB) *AreaHandler {
return &AreaHandler{
service: services.NewAreaService(db),
}
}
// Create godoc
// @Summary Create a new area
// @Description Create a new geographical area
// @Tags Areas
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateAreaRequest true "Area data"
// @Success 201 {object} services.AreaResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 409 {object} map[string]interface{} "Area already exists"
// @Router /areas [post]
func (h *AreaHandler) Create(c *gin.Context) {
var req services.CreateAreaRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.Create(req)
if err != nil {
if err.Error() == "area with same wyId already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, area)
}
// GetByID godoc
// @Summary Get area by ID
// @Description Get a single area by its ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param id path int true "Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/id/{id} [get]
func (h *AreaHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// GetByWyIDAlias godoc
// @Summary Get area by wyId
// @Description Get a single area by its Wyscout ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{wyId} [get]
func (h *AreaHandler) GetByWyIDAlias(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// GetByWyID godoc
// @Summary Get area by WyID
// @Description Get a single area by its Wyscout ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/wy/{wyId} [get]
func (h *AreaHandler) GetByWyID(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// FindAll godoc
// @Summary Get all areas
// @Description Get paginated list of areas with optional filters
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param alpha2 query string false "Filter by alpha2 code"
// @Param alpha3 query string false "Filter by alpha3 code"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedAreasResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /areas [get]
func (h *AreaHandler) FindAll(c *gin.Context) {
var query services.QueryAreasRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update an area
// @Description Update an existing area's information
// @Tags Areas
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Area ID"
// @Param request body services.UpdateAreaRequest true "Area data"
// @Success 200 {object} services.AreaResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{id} [patch]
func (h *AreaHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateAreaRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "area not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, area)
}
// Delete godoc
// @Summary Delete an area
// @Description Soft delete an area
// @Tags Areas
// @Security BearerAuth
// @Param id path int true "Area ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{id} [delete]
func (h *AreaHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "area not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/services"
)
type AuthHandler struct {
service *services.AuthService
}
func NewAuthHandler(db *gorm.DB, cfg config.Config) *AuthHandler {
return &AuthHandler{
service: services.NewAuthService(db, cfg),
}
}
// Login godoc
// @Summary User login
// @Description Authenticate user with email and password
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body services.LoginRequest true "Login credentials"
// @Success 200 {object} services.AuthResponse
// @Failure 400 {object} map[string]interface{} "Invalid request body"
// @Failure 401 {object} map[string]interface{} "Invalid credentials"
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req services.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
deviceInfo := services.DeviceInfo{
UserAgent: c.GetHeader("User-Agent"),
IPAddress: c.ClientIP(),
Platform: c.GetHeader("Sec-Ch-Ua-Platform"),
}
resp, err := h.service.Login(req, deviceInfo)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.JSON(http.StatusOK, resp)
}
// Logout godoc
// @Summary User logout
// @Description Logout user from current device
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Param X-Device-Id header string false "Device ID"
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
deviceID := c.GetHeader("X-Device-Id")
if err := h.service.Logout(userID.(uint), deviceID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// LogoutAll godoc
// @Summary Logout from all devices
// @Description Logout user from all devices
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/logout-all [post]
func (h *AuthHandler) LogoutAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
if err := h.service.LogoutFromAllDevices(userID.(uint)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// Setup2FA godoc
// @Summary Setup two-factor authentication
// @Description Generate 2FA secret and QR code for user
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Success 200 {object} services.Setup2FAResponse
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/setup [post]
func (h *AuthHandler) Setup2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
resp, err := h.service.Setup2FA(userID.(uint))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": err.Error(),
"errorCode": "BAD_REQUEST",
})
return
}
c.JSON(http.StatusOK, resp)
}
// Verify2FA godoc
// @Summary Verify and enable 2FA
// @Description Verify 2FA code and enable two-factor authentication
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{code=string} true "2FA verification code"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]interface{} "Invalid code"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/verify [post]
func (h *AuthHandler) Verify2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.VerifyAndEnable2FA(userID.(uint), req.Code); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": err.Error(),
"errorCode": "BAD_REQUEST",
})
return
}
c.Status(http.StatusNoContent)
}
// Disable2FA godoc
// @Summary Disable two-factor authentication
// @Description Disable 2FA for user account
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{password=string} true "User password for verification"
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/disable [post]
func (h *AuthHandler) Disable2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Disable2FA(userID.(uint), req.Password); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.Status(http.StatusNoContent)
}
// ChangePassword godoc
// @Summary Change user password
// @Description Change the current user's password
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{currentPassword=string,newPassword=string} true "Password change request"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/change-password [post]
func (h *AuthHandler) ChangePassword(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
CurrentPassword string `json:"currentPassword" binding:"required,min=6"`
NewPassword string `json:"newPassword" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.ChangePassword(userID.(uint), req.CurrentPassword, req.NewPassword); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type CalendarHandler struct {
service *services.CalendarService
}
func NewCalendarHandler(db *gorm.DB) *CalendarHandler {
return &CalendarHandler{
service: services.NewCalendarService(db),
}
}
// Create godoc
// @Summary Create a new calendar event
// @Description Create a new calendar event for the authenticated user
// @Tags Calendar
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateCalendarEventRequest true "Event data"
// @Success 201 {object} services.CalendarEventResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /calendar [post]
func (h *CalendarHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateCalendarEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
event, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, event)
}
// GetByID godoc
// @Summary Get calendar event by ID
// @Description Get a single calendar event by its ID
// @Tags Calendar
// @Security BearerAuth
// @Produce json
// @Param id path int true "Event ID"
// @Success 200 {object} services.CalendarEventResponse
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [get]
func (h *CalendarHandler) GetByID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
event, err := h.service.FindByID(userID.(uint), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, event)
}
// FindAll godoc
// @Summary Get all calendar events
// @Description Get paginated list of calendar events with optional filters
// @Tags Calendar
// @Security BearerAuth
// @Produce json
// @Param eventType query string false "Filter by event type"
// @Param fromDate query string false "Filter from date (YYYY-MM-DD)"
// @Param toDate query string false "Filter to date (YYYY-MM-DD)"
// @Param matchWyId query int false "Filter by match WyID"
// @Param playerWyId query int false "Filter by player WyID"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedCalendarEventsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /calendar [get]
func (h *CalendarHandler) FindAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryCalendarEventsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a calendar event
// @Description Update an existing calendar event
// @Tags Calendar
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Event ID"
// @Param request body services.UpdateCalendarEventRequest true "Event data"
// @Success 200 {object} services.CalendarEventResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [patch]
func (h *CalendarHandler) Update(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
var req services.UpdateCalendarEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
event, err := h.service.Update(userID.(uint), id, req)
if err != nil {
if err.Error() == "calendar event not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, event)
}
// Delete godoc
// @Summary Delete a calendar event
// @Description Soft delete a calendar event
// @Tags Calendar
// @Security BearerAuth
// @Param id path int true "Event ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [delete]
func (h *CalendarHandler) Delete(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
if err := h.service.Delete(userID.(uint), id); err != nil {
if err.Error() == "calendar event not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type CoachHandler struct {
service *services.CoachService
}
func NewCoachHandler(db *gorm.DB) *CoachHandler {
return &CoachHandler{
service: services.NewCoachService(db),
}
}
// Save godoc
// @Summary Create or update a coach
// @Description Create a new coach or update existing by WyID
// @Tags Coaches
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateCoachRequest true "Coach data"
// @Success 200 {object} services.CoachResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /coaches [post]
func (h *CoachHandler) Save(c *gin.Context) {
var req services.CreateCoachRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.UpsertByWyID(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByWyIDAlias godoc
// @Summary Get coach by wyId
// @Description Get a single coach by their Wyscout ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{wyId} [get]
func (h *CoachHandler) GetByWyIDAlias(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByID godoc
// @Summary Get coach by ID
// @Description Get a single coach by their internal ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param id path string true "Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/id/{id} [get]
func (h *CoachHandler) GetByID(c *gin.Context) {
id := c.Param("id")
coach, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByWyID godoc
// @Summary Get coach by WyID
// @Description Get a single coach by their Wyscout ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/wy/{wyId} [get]
func (h *CoachHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// FindAll godoc
// @Summary Get all coaches
// @Description Get paginated list of coaches with optional filters
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param teamWyId query int false "Filter by team WyID"
// @Param nationalityWyId query int false "Filter by nationality WyID"
// @Param position query string false "Filter by position"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedCoachesResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /coaches [get]
func (h *CoachHandler) FindAll(c *gin.Context) {
var query services.QueryCoachesRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// UpdateByID godoc
// @Summary Update a coach
// @Description Update an existing coach's information
// @Tags Coaches
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Coach ID"
// @Param request body services.UpdateCoachRequest true "Coach data"
// @Success 200 {object} services.CoachResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{id} [patch]
func (h *CoachHandler) UpdateByID(c *gin.Context) {
id := c.Param("id")
var req services.UpdateCoachRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.UpdateByID(id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// DeleteByID godoc
// @Summary Delete a coach
// @Description Soft delete a coach
// @Tags Coaches
// @Security BearerAuth
// @Param id path string true "Coach ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{id} [delete]
func (h *CoachHandler) DeleteByID(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteByID(id); err != nil {
if err.Error() == "coach not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type FileHandler struct {
service *services.FileService
}
func NewFileHandler(db *gorm.DB) *FileHandler {
return &FileHandler{
service: services.NewFileService(db),
}
}
// Create godoc
// @Summary Create a new file record
// @Description Create a new file metadata record
// @Tags Files
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFileRequest true "File data"
// @Success 201 {object} services.FileResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /files [post]
func (h *FileHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, file)
}
// GetByID godoc
// @Summary Get file by ID
// @Description Get a single file record by its ID
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param id path int true "File ID"
// @Success 200 {object} services.FileResponse
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [get]
func (h *FileHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, file)
}
// FindAll godoc
// @Summary Get all files
// @Description Get paginated list of files with optional filters
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param entityType query string false "Filter by entity type"
// @Param entityId query int false "Filter by entity ID"
// @Param entityWyId query int false "Filter by entity WyID"
// @Param category query string false "Filter by category"
// @Param mimeType query string false "Filter by MIME type"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedFilesResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /files [get]
func (h *FileHandler) FindAll(c *gin.Context) {
var query services.QueryFilesRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// FindByEntity godoc
// @Summary Get files by entity
// @Description Get files associated with a specific entity type
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param entityType path string true "Entity type (player, coach, etc.)"
// @Param entityId query int false "Entity ID"
// @Param entityWyId query int false "Entity WyID"
// @Success 200 {array} services.FileResponse
// @Router /files/entity/{entityType} [get]
func (h *FileHandler) FindByEntity(c *gin.Context) {
entityType := c.Param("entityType")
var entityID *int
var entityWyID *int
if idStr := c.Query("entityId"); idStr != "" {
if id, err := strconv.Atoi(idStr); err == nil {
entityID = &id
}
}
if wyIDStr := c.Query("entityWyId"); wyIDStr != "" {
if wyID, err := strconv.Atoi(wyIDStr); err == nil {
entityWyID = &wyID
}
}
files, err := h.service.FindByEntity(entityType, entityID, entityWyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, files)
}
// Update godoc
// @Summary Update a file record
// @Description Update an existing file metadata record
// @Tags Files
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "File ID"
// @Param request body services.UpdateFileRequest true "File data"
// @Success 200 {object} services.FileResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [patch]
func (h *FileHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "file not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, file)
}
// Delete godoc
// @Summary Delete a file record
// @Description Soft delete a file record
// @Tags Files
// @Security BearerAuth
// @Param id path int true "File ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [delete]
func (h *FileHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "file not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type ListHandler struct {
service *services.ListService
}
func NewListHandler(db *gorm.DB) *ListHandler {
return &ListHandler{
service: services.NewListService(db),
}
}
// Create godoc
// @Summary Create a new list
// @Description Create a new player list for the authenticated user
// @Tags Lists
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateListRequest true "List data"
// @Success 201 {object} services.ListResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /lists [post]
func (h *ListHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateListRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
list, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, list)
}
// GetByID godoc
// @Summary Get list by ID
// @Description Get a single list by its ID
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param id path int true "List ID"
// @Success 200 {object} services.ListResponse
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [get]
func (h *ListHandler) GetByID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
list, err := h.service.FindByID(userID.(uint), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, list)
}
// FindAll godoc
// @Summary Get all lists
// @Description Get paginated list of player lists with optional filters
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param type query string false "Filter by list type"
// @Param name query string false "Filter by name"
// @Param season query string false "Filter by season"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedListsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /lists [get]
func (h *ListHandler) FindAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryListsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// FindSharedWithMe godoc
// @Summary Get lists shared with the authenticated user
// @Description Get paginated list of player lists that have been shared with the authenticated user
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param type query string false "Filter by list type"
// @Param name query string false "Filter by name"
// @Param season query string false "Filter by season"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedListsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /lists/shared-with-me [get]
func (h *ListHandler) FindSharedWithMe(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryListsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindSharedWithUser(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a list
// @Description Update an existing list
// @Tags Lists
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "List ID"
// @Param request body services.UpdateListRequest true "List data"
// @Success 200 {object} services.ListResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [patch]
func (h *ListHandler) Update(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
var req services.UpdateListRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
list, err := h.service.Update(userID.(uint), id, req)
if err != nil {
if err.Error() == "list not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, list)
}
// Delete godoc
// @Summary Delete a list
// @Description Soft delete a list
// @Tags Lists
// @Security BearerAuth
// @Param id path int true "List ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [delete]
func (h *ListHandler) Delete(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
if err := h.service.Delete(userID.(uint), id); err != nil {
if err.Error() == "list not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PlayerAgentHandler struct {
service *services.PlayerAgentService
}
func NewPlayerAgentHandler(db *gorm.DB) *PlayerAgentHandler {
return &PlayerAgentHandler{
service: services.NewPlayerAgentService(db),
}
}
func (h *PlayerAgentHandler) Create(c *gin.Context) {
var req services.CreatePlayerAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
relation, err := h.service.Create(req)
if err != nil {
if err.Error() == "player-agent relation already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, relation)
}
func (h *PlayerAgentHandler) FindByPlayerID(c *gin.Context) {
playerID := c.Param("playerId")
relations, err := h.service.FindByPlayerID(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *PlayerAgentHandler) FindByAgentID(c *gin.Context) {
agentID := c.Param("agentId")
relations, err := h.service.FindByAgentID(agentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *PlayerAgentHandler) Delete(c *gin.Context) {
playerID := c.Param("playerId")
agentID := c.Param("agentId")
if err := h.service.Delete(playerID, agentID); err != nil {
if err.Error() == "player-agent relation not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
type CoachAgentHandler struct {
service *services.CoachAgentService
}
func NewCoachAgentHandler(db *gorm.DB) *CoachAgentHandler {
return &CoachAgentHandler{
service: services.NewCoachAgentService(db),
}
}
func (h *CoachAgentHandler) Create(c *gin.Context) {
var req services.CreateCoachAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
relation, err := h.service.Create(req)
if err != nil {
if err.Error() == "coach-agent relation already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, relation)
}
func (h *CoachAgentHandler) FindByCoachID(c *gin.Context) {
coachID := c.Param("coachId")
relations, err := h.service.FindByCoachID(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *CoachAgentHandler) FindByAgentID(c *gin.Context) {
agentID := c.Param("agentId")
relations, err := h.service.FindByAgentID(agentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *CoachAgentHandler) Delete(c *gin.Context) {
coachID := c.Param("coachId")
agentID := c.Param("agentId")
if err := h.service.Delete(coachID, agentID); err != nil {
if err.Error() == "coach-agent relation not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PlayerHandler struct {
service *services.PlayerService
}
func NewPlayerHandler(db *gorm.DB) *PlayerHandler {
return &PlayerHandler{
service: services.NewPlayerService(db),
}
}
// Save godoc
// @Summary Create or update a player
// @Description Create a new player or update existing by WyID
// @Tags Players
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreatePlayerRequest true "Player data"
// @Success 200 {object} services.PlayerResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /players [post]
func (h *PlayerHandler) Save(c *gin.Context) {
var req services.CreatePlayerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.UpsertByWyID(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, player)
}
// GetByID godoc
// @Summary Get player by ID
// @Description Get a single player by their internal ID
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param id path string true "Player ID"
// @Success 200 {object} services.PlayerResponse
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [get]
func (h *PlayerHandler) GetByID(c *gin.Context) {
id := c.Param("id")
player, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// GetByWyID godoc
// @Summary Get player by WyID
// @Description Get a single player by their Wyscout ID
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Player ID"
// @Success 200 {object} services.PlayerResponse
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/wy/{wyId} [get]
func (h *PlayerHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// FindAll godoc
// @Summary Get all players
// @Description Get paginated list of players with optional filters
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param foot query string false "Filter by preferred foot"
// @Param teamWyId query int false "Filter by team WyID"
// @Param nationalityWyId query int false "Filter by nationality WyID"
// @Param positions query string false "Filter by positions (comma-separated)"
// @Param minAge query int false "Minimum age"
// @Param maxAge query int false "Maximum age"
// @Param minHeight query int false "Minimum height in cm"
// @Param maxHeight query int false "Maximum height in cm"
// @Param gender query string false "Filter by gender"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedPlayersResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /players [get]
func (h *PlayerHandler) FindAll(c *gin.Context) {
var query services.QueryPlayersRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// UpdateByID godoc
// @Summary Update a player
// @Description Update an existing player's information
// @Tags Players
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Player ID"
// @Param request body services.UpdatePlayerRequest true "Player data"
// @Success 200 {object} services.PlayerResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [patch]
func (h *PlayerHandler) UpdateByID(c *gin.Context) {
id := c.Param("id")
var req services.UpdatePlayerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.UpdateByID(id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// DeleteByID godoc
// @Summary Delete a player
// @Description Soft delete a player
// @Tags Players
// @Security BearerAuth
// @Param id path string true "Player ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [delete]
func (h *PlayerHandler) DeleteByID(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteByID(id); err != nil {
if err.Error() == "player not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PositionHandler struct {
service *services.PositionService
}
func NewPositionHandler(db *gorm.DB) *PositionHandler {
return &PositionHandler{
service: services.NewPositionService(db),
}
}
// Create godoc
// @Summary Create a new position
// @Description Create a new player position
// @Tags Positions
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreatePositionRequest true "Position data"
// @Success 201 {object} services.PositionResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /positions [post]
func (h *PositionHandler) Create(c *gin.Context) {
var req services.CreatePositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, position)
}
// GetByID godoc
// @Summary Get position by ID
// @Description Get a single position by its ID
// @Tags Positions
// @Security BearerAuth
// @Produce json
// @Param id path int true "Position ID"
// @Success 200 {object} services.PositionResponse
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [get]
func (h *PositionHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, position)
}
// FindAll godoc
// @Summary Get all positions
// @Description Get paginated list of positions with optional filters
// @Tags Positions
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param category query string false "Filter by category"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedPositionsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /positions [get]
func (h *PositionHandler) FindAll(c *gin.Context) {
var query services.QueryPositionsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a position
// @Description Update an existing position
// @Tags Positions
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Position ID"
// @Param request body services.UpdatePositionRequest true "Position data"
// @Success 200 {object} services.PositionResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [patch]
func (h *PositionHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdatePositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "position not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, position)
}
// Delete godoc
// @Summary Delete a position
// @Description Soft delete a position
// @Tags Positions
// @Security BearerAuth
// @Param id path int true "Position ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [delete]
func (h *PositionHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "position not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type ReportHandler struct {
service *services.ReportService
}
func NewReportHandler(db *gorm.DB) *ReportHandler {
return &ReportHandler{
service: services.NewReportService(db),
}
}
// Create godoc
// @Summary Create a new report
// @Description Create a new scouting report
// @Tags Reports
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateReportRequest true "Report data"
// @Success 201 {object} services.ReportResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /reports [post]
func (h *ReportHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("POST /reports invalid request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"details": err.Error(),
"errorCode": "VALIDATION_ERROR",
})
return
}
report, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, report)
}
// GetByID godoc
// @Summary Get report by ID
// @Description Get a single report by its ID
// @Tags Reports
// @Security BearerAuth
// @Produce json
// @Param id path int true "Report ID"
// @Success 200 {object} services.ReportResponse
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [get]
func (h *ReportHandler) GetByID(c *gin.Context) {
id := c.Param("id")
report, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, report)
}
// FindAll godoc
// @Summary Get all reports
// @Description Get paginated list of reports with optional filters
// @Tags Reports
// @Security BearerAuth
// @Produce json
// @Param playerWyId query int false "Filter by player WyID"
// @Param coachWyId query int false "Filter by coach WyID"
// @Param matchWyId query int false "Filter by match WyID"
// @Param type query string false "Filter by report type"
// @Param status query string false "Filter by status"
// @Param userId query int false "Filter by user ID"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedReportsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /reports [get]
func (h *ReportHandler) FindAll(c *gin.Context) {
var query services.QueryReportsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a report
// @Description Update an existing report's information
// @Tags Reports
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Report ID"
// @Param request body services.UpdateReportRequest true "Report data"
// @Success 200 {object} services.ReportResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [patch]
func (h *ReportHandler) Update(c *gin.Context) {
id := c.Param("id")
var req services.UpdateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
report, err := h.service.Update(id, req)
if err != nil {
if err.Error() == "report not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, report)
}
// Delete godoc
// @Summary Delete a report
// @Description Soft delete a report
// @Tags Reports
// @Security BearerAuth
// @Param id path int true "Report ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [delete]
func (h *ReportHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := h.service.Delete(id); err != nil {
if err.Error() == "report not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type UserHandler struct {
service *services.UserService
}
func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{
service: services.NewUserService(db),
}
}
// FindAll godoc
// @Summary Get all users
// @Description Get paginated list of users with optional filters
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param name query string false "Filter by name"
// @Param email query string false "Filter by email"
// @Param role query string false "Filter by role"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {array} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /users [get]
func (h *UserHandler) FindAll(c *gin.Context) {
var query services.QueryUsersRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
users, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, users)
}
// FindOne godoc
// @Summary Get user by ID
// @Description Get a single user by their ID
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id} [get]
func (h *UserHandler) FindOne(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.FindOne(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// Create godoc
// @Summary Create a new user
// @Description Create a new user account
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateUserRequest true "User data"
// @Success 201 {object} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 409 {object} map[string]interface{} "Email already exists"
// @Router /users [post]
func (h *UserHandler) Create(c *gin.Context) {
var req services.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Create(req)
if err != nil {
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, user)
}
// Update godoc
// @Summary Update a user
// @Description Update an existing user's information
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param request body services.UpdateUserRequest true "User data"
// @Success 200 {object} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "User not found"
// @Failure 409 {object} map[string]interface{} "Email already exists"
// @Router /users/{id} [patch]
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, user)
}
// Remove godoc
// @Summary Delete a user
// @Description Soft delete a user account
// @Tags Users
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id} [delete]
func (h *UserHandler) Remove(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Remove(uint(id)); err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// Activate godoc
// @Summary Activate a user
// @Description Activate a user account
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id}/activate [post]
func (h *UserHandler) Activate(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Activate(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// Deactivate godoc
// @Summary Deactivate a user
// @Description Deactivate a user account
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id}/deactivate [post]
func (h *UserHandler) Deactivate(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Deactivate(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
package middleware
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type Claims struct {
UserID uint `json:"userId"`
jwt.RegisteredClaims
}
func JWTAuth(db *gorm.DB, jwtSecret string) gin.HandlerFunc {
secret := []byte(jwtSecret)
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
tokenStr := ""
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
tokenStr = strings.TrimSpace(authHeader[len("Bearer "):])
} else {
// Swagger UI / manual clients sometimes send the raw JWT without the "Bearer " prefix.
// Accept it if it looks like a JWT (three dot-separated segments).
parts := strings.Split(authHeader, ".")
if len(parts) == 3 {
tokenStr = strings.TrimSpace(authHeader)
}
}
if tokenStr == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
claims := &Claims{}
tkn, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil || !tkn.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
var session models.UserSession
if err := db.Where("token = ?", tokenStr).First(&session).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
if time.Now().After(session.ExpiresAt) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
var user models.User
if err := db.Where("id = ?", claims.UserID).First(&user).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
if user.IsActive != nil && !*user.IsActive {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
c.Set("user", user)
c.Set("userId", user.ID)
c.Set("session", session)
c.Next()
}
}
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"ScoutSystemElite/internal/models"
)
var AllRoles = []string{
"superadmin",
"admin",
"groupmanager",
"president",
"sportsdirector",
"chiefscout",
"staffscout",
"chiefdataanalyst",
"staffdataanalyst",
"chieftransfermarket",
"stafftransfermarket",
"viewer",
}
func RequireRoles(allowedRoles ...string) gin.HandlerFunc {
roleSet := make(map[string]bool)
for _, r := range allowedRoles {
roleSet[r] = true
}
return func(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
user, ok := userVal.(models.User)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
if !roleSet[user.Role] {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"statusCode": http.StatusForbidden,
"message": "Forbidden - insufficient permissions",
"errorCode": "FORBIDDEN",
})
return
}
c.Next()
}
}
func AdminOnly() gin.HandlerFunc {
return RequireRoles("superadmin", "admin")
}
func SuperAdminOnly() gin.HandlerFunc {
return RequireRoles("superadmin")
}
func ManagersAndAbove() gin.HandlerFunc {
return RequireRoles(
"superadmin",
"admin",
"groupmanager",
"president",
"sportsdirector",
"chiefscout",
"chiefdataanalyst",
"chieftransfermarket",
)
}
func AllAuthenticated() gin.HandlerFunc {
return RequireRoles(AllRoles...)
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type AgentService struct {
db *gorm.DB
}
func NewAgentService(db *gorm.DB) *AgentService {
return &AgentService{db: db}
}
type AgentResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Type string `json:"type"`
Email string `json:"email"`
Phone string `json:"phone"`
Status string `json:"status"`
Address string `json:"address"`
CountryWyID int `json:"countryWyId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryAgentsRequest struct {
Name string `form:"name"`
Type string `form:"type"`
Status string `form:"status"`
CountryWyID *int `form:"countryWyId"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateAgentRequest struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Type string `json:"type" binding:"required"`
Email string `json:"email" binding:"required,email"`
Phone string `json:"phone" binding:"required"`
Status string `json:"status" binding:"required"`
Address string `json:"address" binding:"required"`
CountryWyID int `json:"countryWyId" binding:"required"`
}
type UpdateAgentRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Type *string `json:"type"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Status *string `json:"status"`
Address *string `json:"address"`
CountryWyID *int `json:"countryWyId"`
}
type PaginatedAgentsResponse struct {
Data []AgentResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
type AgentPlayerAssociationResponse struct {
ID uint `json:"id"`
AgentID string `json:"agentId"`
PlayerID string `json:"playerId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type AgentCoachAssociationResponse struct {
ID uint `json:"id"`
AgentID string `json:"agentId"`
CoachID string `json:"coachId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (s *AgentService) Create(req CreateAgentRequest) (*AgentResponse, error) {
agent := models.Agent{
Name: req.Name,
Description: req.Description,
Type: req.Type,
Email: req.Email,
Phone: req.Phone,
Status: req.Status,
Address: req.Address,
CountryWyID: req.CountryWyID,
}
if err := s.db.Create(&agent).Error; err != nil {
return nil, err
}
return toAgentResponse(agent), nil
}
func (s *AgentService) FindByID(id string) (*AgentResponse, error) {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("agent not found")
}
return nil, err
}
return toAgentResponse(agent), nil
}
func (s *AgentService) FindAll(query QueryAgentsRequest) (*PaginatedAgentsResponse, error) {
q := s.db.Model(&models.Agent{})
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Type != "" {
q = q.Where("type = ?", query.Type)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
if query.CountryWyID != nil {
q = q.Where("country_wy_id = ?", *query.CountryWyID)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"type": "type",
"status": "status",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var agents []models.Agent
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&agents).Error; err != nil {
return nil, err
}
data := make([]AgentResponse, len(agents))
for i, a := range agents {
data[i] = *toAgentResponse(a)
}
return &PaginatedAgentsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(agents)) < total,
}, nil
}
func (s *AgentService) Update(id string, req UpdateAgentRequest) (*AgentResponse, error) {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("agent not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Type != nil {
updates["type"] = *req.Type
}
if req.Email != nil {
updates["email"] = *req.Email
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.Address != nil {
updates["address"] = *req.Address
}
if req.CountryWyID != nil {
updates["country_wy_id"] = *req.CountryWyID
}
if len(updates) > 0 {
if err := s.db.Model(&agent).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&agent, "id = ?", id)
}
return toAgentResponse(agent), nil
}
func (s *AgentService) Delete(id string) error {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("agent not found")
}
return err
}
return s.db.Delete(&agent).Error
}
func (s *AgentService) AssociateWithPlayer(agentID string, playerID string) (*AgentPlayerAssociationResponse, error) {
var existing models.PlayerAgent
if err := s.db.Where("agent_id = ? AND player_id = ?", agentID, playerID).First(&existing).Error; err == nil {
return toAgentPlayerAssociationResponse(existing), nil
}
pa := models.PlayerAgent{AgentID: agentID, PlayerID: playerID}
if err := s.db.Create(&pa).Error; err != nil {
return nil, err
}
return toAgentPlayerAssociationResponse(pa), nil
}
func (s *AgentService) AssociateWithCoach(agentID string, coachID string) (*AgentCoachAssociationResponse, error) {
var existing models.CoachAgent
if err := s.db.Where("agent_id = ? AND coach_id = ?", agentID, coachID).First(&existing).Error; err == nil {
return toAgentCoachAssociationResponse(existing), nil
}
ca := models.CoachAgent{AgentID: agentID, CoachID: coachID}
if err := s.db.Create(&ca).Error; err != nil {
return nil, err
}
return toAgentCoachAssociationResponse(ca), nil
}
func (s *AgentService) RemoveFromPlayer(agentID string, playerID string) error {
return s.db.Where("agent_id = ? AND player_id = ?", agentID, playerID).Delete(&models.PlayerAgent{}).Error
}
func (s *AgentService) RemoveFromCoach(agentID string, coachID string) error {
return s.db.Where("agent_id = ? AND coach_id = ?", agentID, coachID).Delete(&models.CoachAgent{}).Error
}
func (s *AgentService) GetAgentsByPlayer(playerID string) ([]AgentResponse, error) {
var agents []models.Agent
if err := s.db.
Model(&models.Agent{}).
Joins("JOIN player_agents ON player_agents.agent_id = agents.id").
Where("player_agents.player_id = ?", playerID).
Order("agents.name asc").
Find(&agents).Error; err != nil {
return nil, err
}
res := make([]AgentResponse, len(agents))
for i, a := range agents {
res[i] = *toAgentResponse(a)
}
return res, nil
}
func (s *AgentService) GetAgentsByCoach(coachID string) ([]AgentResponse, error) {
var agents []models.Agent
if err := s.db.
Model(&models.Agent{}).
Joins("JOIN coach_agents ON coach_agents.agent_id = agents.id").
Where("coach_agents.coach_id = ?", coachID).
Order("agents.name asc").
Find(&agents).Error; err != nil {
return nil, err
}
res := make([]AgentResponse, len(agents))
for i, a := range agents {
res[i] = *toAgentResponse(a)
}
return res, nil
}
func toAgentResponse(a models.Agent) *AgentResponse {
return &AgentResponse{
ID: a.ID,
Name: a.Name,
Description: a.Description,
Type: a.Type,
Email: a.Email,
Phone: a.Phone,
Status: a.Status,
Address: a.Address,
CountryWyID: a.CountryWyID,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}
func toAgentPlayerAssociationResponse(pa models.PlayerAgent) *AgentPlayerAssociationResponse {
return &AgentPlayerAssociationResponse{
ID: pa.ID,
AgentID: pa.AgentID,
PlayerID: pa.PlayerID,
CreatedAt: pa.CreatedAt,
UpdatedAt: pa.UpdatedAt,
}
}
func toAgentCoachAssociationResponse(ca models.CoachAgent) *AgentCoachAssociationResponse {
return &AgentCoachAssociationResponse{
ID: ca.ID,
AgentID: ca.AgentID,
CoachID: ca.CoachID,
CreatedAt: ca.CreatedAt,
UpdatedAt: ca.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type AreaService struct {
db *gorm.DB
}
func NewAreaService(db *gorm.DB) *AreaService {
return &AreaService{db: db}
}
type AreaResponse struct {
ID uint `json:"id"`
WyID int `json:"wyId"`
Name string `json:"name"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type QueryAreasRequest struct {
Name string `form:"name"`
Alpha2 string `form:"alpha2"`
Alpha3 string `form:"alpha3"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateAreaRequest struct {
WyID int `json:"wyId" binding:"required"`
Name string `json:"name" binding:"required"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
}
type UpdateAreaRequest struct {
WyID *int `json:"wyId"`
Name *string `json:"name"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
}
type PaginatedAreasResponse struct {
Data []AreaResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *AreaService) Create(req CreateAreaRequest) (*AreaResponse, error) {
var existing models.Area
if err := s.db.Where("wy_id = ?", req.WyID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("area with same wyId already exists")
}
area := models.Area{
WyID: req.WyID,
Name: req.Name,
Alpha2Code: req.Alpha2Code,
Alpha3Code: req.Alpha3Code,
}
if err := s.db.Create(&area).Error; err != nil {
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindByID(id uint) (*AreaResponse, error) {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindByWyID(wyID int) (*AreaResponse, error) {
var area models.Area
if err := s.db.Where("wy_id = ?", wyID).First(&area).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindAll(query QueryAreasRequest) (*PaginatedAreasResponse, error) {
q := s.db.Model(&models.Area{})
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Alpha2 != "" {
q = q.Where("alpha2code = ?", strings.ToUpper(query.Alpha2))
}
if query.Alpha3 != "" {
q = q.Where("alpha3code = ?", strings.ToUpper(query.Alpha3))
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"wyId": "wy_id",
"alpha2": "alpha2code",
"alpha3": "alpha3code",
"createdAt": "created_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var areas []models.Area
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&areas).Error; err != nil {
return nil, err
}
data := make([]AreaResponse, len(areas))
for i, a := range areas {
data[i] = *toAreaResponse(a)
}
return &PaginatedAreasResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(areas)) < total,
}, nil
}
func (s *AreaService) Update(id uint, req UpdateAreaRequest) (*AreaResponse, error) {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.WyID != nil {
updates["wy_id"] = *req.WyID
}
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Alpha2Code != nil {
updates["alpha2code"] = *req.Alpha2Code
}
if req.Alpha3Code != nil {
updates["alpha3code"] = *req.Alpha3Code
}
if len(updates) > 0 {
if err := s.db.Model(&area).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&area, id)
}
return toAreaResponse(area), nil
}
func (s *AreaService) Delete(id uint) error {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("area not found")
}
return err
}
return s.db.Delete(&area).Error
}
func toAreaResponse(a models.Area) *AreaResponse {
return &AreaResponse{
ID: a.ID,
WyID: a.WyID,
Name: a.Name,
Alpha2Code: a.Alpha2Code,
Alpha3Code: a.Alpha3Code,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
DeletedAt: a.DeletedAt,
}
}
package services
import (
"crypto/rand"
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/models"
)
type AuthService struct {
db *gorm.DB
cfg config.Config
}
func NewAuthService(db *gorm.DB, cfg config.Config) *AuthService {
return &AuthService{db: db, cfg: cfg}
}
type DeviceInfo struct {
UserAgent string `json:"userAgent"`
IPAddress string `json:"ipAddress"`
Platform string `json:"platform"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
TwoFactorCode string `json:"twoFactorCode,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
}
type UserInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
}
type AuthResponse struct {
AccessToken string `json:"accessToken"`
User UserInfo `json:"user"`
RequiresTwoFactor bool `json:"requiresTwoFactor,omitempty"`
}
type Setup2FAResponse struct {
QRCodeURL string `json:"qrCodeUrl"`
Secret string `json:"secret"`
BackupCodes []string `json:"backupCodes"`
}
func (s *AuthService) Login(req LoginRequest, deviceInfo DeviceInfo) (*AuthResponse, error) {
var user models.User
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("invalid credentials")
}
return nil, err
}
if user.IsActive != nil && !*user.IsActive {
return nil, fmt.Errorf("invalid credentials")
}
if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
return nil, fmt.Errorf("account is locked due to multiple failed login attempts")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
s.handleFailedLogin(user.ID)
return nil, fmt.Errorf("invalid credentials")
}
if user.FailedLoginAttempts != nil && *user.FailedLoginAttempts > 0 {
s.resetFailedLoginAttempts(user.ID)
}
twoFactorEnabled := user.TwoFactorEnabled != nil && *user.TwoFactorEnabled
if twoFactorEnabled {
if req.TwoFactorCode == "" {
return &AuthResponse{
AccessToken: "",
User: UserInfo{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
TwoFactorEnabled: true,
},
RequiresTwoFactor: true,
}, nil
}
if user.TwoFactorSecret == nil || !totp.Validate(req.TwoFactorCode, *user.TwoFactorSecret) {
return nil, fmt.Errorf("invalid 2FA code")
}
}
deviceID := req.DeviceID
if deviceID == "" {
deviceID = generateDeviceID()
}
s.db.Where("user_id = ?", user.ID).Delete(&models.UserSession{})
token, err := s.createSession(user, deviceID, deviceInfo)
if err != nil {
return nil, err
}
now := time.Now()
s.db.Model(&user).Update("last_login_at", now)
return &AuthResponse{
AccessToken: token,
User: UserInfo{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
TwoFactorEnabled: twoFactorEnabled,
},
}, nil
}
func (s *AuthService) Logout(userID uint, deviceID string) error {
if deviceID != "" {
return s.db.Where("user_id = ? AND device_id = ?", userID, deviceID).Delete(&models.UserSession{}).Error
}
return s.LogoutFromAllDevices(userID)
}
func (s *AuthService) LogoutFromAllDevices(userID uint) error {
return s.db.Where("user_id = ?", userID).Delete(&models.UserSession{}).Error
}
func (s *AuthService) Setup2FA(userID uint) (*Setup2FAResponse, error) {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return nil, fmt.Errorf("user not found")
}
if user.TwoFactorEnabled != nil && *user.TwoFactorEnabled {
return nil, fmt.Errorf("2FA is already enabled for this user")
}
secret := make([]byte, 20)
if _, err := rand.Read(secret); err != nil {
return nil, err
}
secretBase32 := base32.StdEncoding.EncodeToString(secret)
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "ScoutingSystem",
AccountName: user.Email,
Secret: []byte(secretBase32),
})
if err != nil {
return nil, err
}
backupCodes := generateBackupCodes(8)
s.db.Model(&user).Update("two_factor_secret", key.Secret())
return &Setup2FAResponse{
QRCodeURL: key.URL(),
Secret: key.Secret(),
BackupCodes: backupCodes,
}, nil
}
func (s *AuthService) VerifyAndEnable2FA(userID uint, code string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if user.TwoFactorSecret == nil || *user.TwoFactorSecret == "" {
return fmt.Errorf("2FA setup not initiated")
}
if !totp.Validate(code, *user.TwoFactorSecret) {
return fmt.Errorf("invalid 2FA code")
}
enabled := true
return s.db.Model(&user).Update("two_factor_enabled", enabled).Error
}
func (s *AuthService) Disable2FA(userID uint, password string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return fmt.Errorf("invalid password")
}
s.db.Where("user_id = ?", userID).Delete(&models.UserSession{})
return s.db.Model(&user).Updates(map[string]interface{}{
"two_factor_enabled": false,
"two_factor_secret": nil,
}).Error
}
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
return fmt.Errorf("invalid current password")
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.db.Model(&user).Update("password_hash", string(hash)).Error
}
func (s *AuthService) createSession(user models.User, deviceID string, deviceInfo DeviceInfo) (string, error) {
ttlMinutes, _ := strconv.Atoi(s.cfg.JWTAccessTokenTTLMinutes)
if ttlMinutes == 0 {
ttlMinutes = 1440
}
expiresAt := time.Now().Add(time.Duration(ttlMinutes) * time.Minute)
claims := jwt.MapClaims{
"userId": user.ID,
"email": user.Email,
"role": user.Role,
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(s.cfg.JWTSecret))
if err != nil {
return "", err
}
deviceInfoJSON, _ := json.Marshal(deviceInfo)
session := models.UserSession{
UserID: user.ID,
Token: tokenString,
DeviceID: deviceID,
DeviceInfo: datatypes.JSON(deviceInfoJSON),
ExpiresAt: expiresAt,
}
if err := s.db.Create(&session).Error; err != nil {
return "", err
}
return tokenString, nil
}
func (s *AuthService) handleFailedLogin(userID uint) {
var user models.User
s.db.First(&user, userID)
attempts := 1
if user.FailedLoginAttempts != nil {
attempts = *user.FailedLoginAttempts + 1
}
updates := map[string]interface{}{
"failed_login_attempts": attempts,
}
if attempts >= 5 {
lockUntil := time.Now().Add(30 * time.Minute)
updates["locked_until"] = lockUntil
}
s.db.Model(&user).Updates(updates)
}
func (s *AuthService) resetFailedLoginAttempts(userID uint) {
s.db.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"failed_login_attempts": 0,
"locked_until": nil,
})
}
func generateDeviceID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("device_%x", b)
}
func generateBackupCodes(count int) []string {
codes := make([]string, count)
for i := 0; i < count; i++ {
b := make([]byte, 4)
rand.Read(b)
codes[i] = fmt.Sprintf("%08X", b)
}
return codes
}
package services
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type CalendarService struct {
db *gorm.DB
}
func NewCalendarService(db *gorm.DB) *CalendarService {
return &CalendarService{db: db}
}
type CalendarEventResponse struct {
ID string `json:"id"`
UserID uint `json:"userId"`
Title string `json:"title"`
Description *string `json:"description"`
EventType string `json:"eventType"`
StartDate time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryCalendarEventsRequest struct {
EventType string `form:"eventType"`
FromDate string `form:"fromDate"`
ToDate string `form:"toDate"`
MatchWyID *int `form:"matchWyId"`
PlayerWyID *int `form:"playerWyId"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateCalendarEventRequest struct {
Title string `json:"title" binding:"required"`
Description *string `json:"description"`
EventType string `json:"eventType" binding:"required"`
StartDate time.Time `json:"startDate" binding:"required"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
}
type UpdateCalendarEventRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
EventType *string `json:"eventType"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
IsActive *bool `json:"isActive"`
}
type PaginatedCalendarEventsResponse struct {
Data []CalendarEventResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *CalendarService) Create(userID uint, req CreateCalendarEventRequest) (*CalendarEventResponse, error) {
eventType := models.CalendarEventType(req.EventType)
isActive := true
metaJSON, err := json.Marshal(req.Metadata)
if err != nil {
return nil, err
}
event := models.CalendarEvent{
UserID: userID,
Title: req.Title,
Description: req.Description,
EventType: eventType,
StartDate: req.StartDate,
EndDate: req.EndDate,
MatchWyID: req.MatchWyID,
PlayerWyID: req.PlayerWyID,
Metadata: datatypes.JSON(metaJSON),
IsActive: &isActive,
}
if err := s.db.Create(&event).Error; err != nil {
return nil, err
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) FindByID(userID uint, id string) (*CalendarEventResponse, error) {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("calendar event not found")
}
return nil, err
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) FindAll(userID uint, query QueryCalendarEventsRequest) (*PaginatedCalendarEventsResponse, error) {
q := s.db.Model(&models.CalendarEvent{}).Where("user_id = ? AND deleted_at IS NULL", userID)
if query.EventType != "" {
q = q.Where("event_type = ?", query.EventType)
}
if query.FromDate != "" {
if t, err := time.Parse("2006-01-02", query.FromDate); err == nil {
q = q.Where("start_date >= ?", t)
}
}
if query.ToDate != "" {
if t, err := time.Parse("2006-01-02", query.ToDate); err == nil {
q = q.Where("start_date <= ?", t)
}
}
if query.MatchWyID != nil {
q = q.Where("match_wy_id = ?", *query.MatchWyID)
}
if query.PlayerWyID != nil {
q = q.Where("player_wy_id = ?", *query.PlayerWyID)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "start_date"
allowedSorts := map[string]string{
"title": "title",
"eventType": "event_type",
"startDate": "start_date",
"endDate": "end_date",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var events []models.CalendarEvent
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&events).Error; err != nil {
return nil, err
}
data := make([]CalendarEventResponse, len(events))
for i, e := range events {
data[i] = *toCalendarEventResponse(e)
}
return &PaginatedCalendarEventsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(events)) < total,
}, nil
}
func (s *CalendarService) Update(userID uint, id string, req UpdateCalendarEventRequest) (*CalendarEventResponse, error) {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("calendar event not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.EventType != nil {
updates["event_type"] = *req.EventType
}
if req.StartDate != nil {
updates["start_date"] = *req.StartDate
}
if req.EndDate != nil {
updates["end_date"] = *req.EndDate
}
if req.MatchWyID != nil {
updates["match_wy_id"] = *req.MatchWyID
}
if req.PlayerWyID != nil {
updates["player_wy_id"] = *req.PlayerWyID
}
if req.Metadata != nil {
metaJSON, err := json.Marshal(req.Metadata)
if err != nil {
return nil, err
}
updates["metadata"] = datatypes.JSON(metaJSON)
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&event).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&event, "id = ?", id)
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) Delete(userID uint, id string) error {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("calendar event not found")
}
return err
}
now := time.Now()
return s.db.Model(&event).Update("deleted_at", now).Error
}
func toCalendarEventResponse(e models.CalendarEvent) *CalendarEventResponse {
isActive := e.IsActive != nil && *e.IsActive
var meta interface{}
if len(e.Metadata) > 0 {
_ = json.Unmarshal([]byte(e.Metadata), &meta)
}
return &CalendarEventResponse{
ID: e.ID,
UserID: e.UserID,
Title: e.Title,
Description: e.Description,
EventType: string(e.EventType),
StartDate: e.StartDate,
EndDate: e.EndDate,
MatchWyID: e.MatchWyID,
PlayerWyID: e.PlayerWyID,
Metadata: meta,
IsActive: isActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment