├── bin └── .keep ├── README.md ├── .gitignore ├── .env ├── Makefile ├── Dockerfile ├── db ├── auto-migrate.go └── database.go ├── utils └── jwt.go ├── models ├── user.go ├── auth.go ├── ticket.go └── event.go ├── config └── config.go ├── repositories ├── auth.go ├── ticket.go └── event.go ├── docker-compose.yaml ├── cmd └── api │ └── main.go ├── go.mod ├── middlewares └── auth-protected.go ├── services └── auth.go ├── .air.toml ├── handlers ├── auth.go ├── event.go └── ticket.go └── go.sum /bin/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ To view the final code, make sure to select the main branch ⚠️ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Air 2 | post_cmd.txt 3 | pre_cmd.txt 4 | tmp 5 | 6 | # Binaries 7 | bin/* 8 | !bin/.keep 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SERVER_PORT=8081 2 | 3 | DB_HOST=db 4 | DB_NAME=postgres 5 | DB_USER=postgres 6 | DB_PASSWORD=postgres 7 | DB_SSLMODE=disable 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @docker compose up --build 3 | 4 | stop: 5 | @docker-compose rm -v --force --stop 6 | @docker rmi ticket-booking 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.2-alpine3.19 2 | 3 | WORKDIR /src/app 4 | 5 | RUN go install github.com/cosmtrek/air@latest 6 | 7 | COPY . . 8 | 9 | RUN go mod tidy 10 | -------------------------------------------------------------------------------- /db/auto-migrate.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/mathvaillant/ticket-booking-project-v0/models" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func DBMigrator(db *gorm.DB) error { 9 | return db.AutoMigrate(&models.Event{}, &models.Ticket{}, &models.User{}) 10 | } 11 | -------------------------------------------------------------------------------- /utils/jwt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v5" 5 | ) 6 | 7 | func GenerateJWT(claims jwt.Claims, method jwt.SigningMethod, jwtSecret string) (string, error) { 8 | return jwt.NewWithClaims(method, claims).SignedString([]byte(jwtSecret)) 9 | } 10 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type UserRole string 10 | 11 | const ( 12 | Manager UserRole = "manager" 13 | attendee UserRole = "attendee" 14 | ) 15 | 16 | type User struct { 17 | ID uint `json:"id" gorm:"primarykey"` 18 | Email string `json:"email" gorm:"text;not null"` 19 | Role UserRole `json:"role" gorm:"text;default:attendee"` 20 | Password string `json:"-"` // Do not compute the password in json 21 | CreatedAt time.Time `json:"createdAt"` 22 | UpdatedAt time.Time `json:"updatedAt"` 23 | } 24 | 25 | func (u *User) AfterCreate(db *gorm.DB) (err error) { 26 | if u.ID == 1 { 27 | db.Model(u).Update("role", Manager) 28 | } 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/caarlos0/env" 5 | "github.com/gofiber/fiber/v2/log" 6 | "github.com/joho/godotenv" 7 | ) 8 | 9 | type EnvConfig struct { 10 | ServerPort string `env:"SERVER_PORT,required"` 11 | DBHost string `env:"DB_HOST,required"` 12 | DBName string `env:"DB_NAME,required"` 13 | DBUser string `env:"DB_USER,required"` 14 | DBPassword string `env:"DB_PASSWORD,required"` 15 | DBSSLMode string `env:"DB_SSLMODE,required"` 16 | } 17 | 18 | func NewEnvConfig() *EnvConfig { 19 | err := godotenv.Load() 20 | 21 | if err != nil { 22 | log.Fatalf("Unable to load .env: %e", err) 23 | } 24 | 25 | config := &EnvConfig{} 26 | 27 | if err := env.Parse(config); err != nil { 28 | log.Fatalf("Unable to load variables from .env: %e", err) 29 | } 30 | 31 | return config 32 | } 33 | -------------------------------------------------------------------------------- /db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gofiber/fiber/v2/log" 7 | "github.com/mathvaillant/ticket-booking-project-v0/config" 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/logger" 11 | ) 12 | 13 | func Init(config *config.EnvConfig, DBMigrator func(*gorm.DB) error) *gorm.DB { 14 | uri := fmt.Sprintf(` 15 | host=%s user=%s dbname=%s password=%s sslmode=%s port=5432`, 16 | config.DBHost, config.DBUser, config.DBName, config.DBPassword, config.DBSSLMode, 17 | ) 18 | 19 | db, err := gorm.Open(postgres.Open(uri), &gorm.Config{ 20 | Logger: logger.Default.LogMode(logger.Info), 21 | }) 22 | 23 | if err != nil { 24 | log.Fatalf("Unable to connect to database: %v", err) 25 | } 26 | 27 | log.Info("Connected to the database") 28 | 29 | if err := DBMigrator(db); err != nil { 30 | log.Fatalf("Unable to migrate: %v", err) 31 | } 32 | 33 | return db 34 | } 35 | -------------------------------------------------------------------------------- /models/auth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "net/mail" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | type AuthCredentials struct { 11 | Email string `json:"email" validate:"required"` 12 | Password string `json:"password" validate:"required"` 13 | } 14 | 15 | type AuthRepository interface { 16 | RegisterUser(ctx context.Context, registerData *AuthCredentials) (*User, error) 17 | GetUser(ctx context.Context, query interface{}, args ...interface{}) (*User, error) 18 | } 19 | 20 | type AuthService interface { 21 | Login(ctx context.Context, loginData *AuthCredentials) (string, *User, error) 22 | Register(ctx context.Context, registerData *AuthCredentials) (string, *User, error) 23 | } 24 | 25 | // Check if a password matches a hash 26 | func MatchesHash(password, hash string) bool { 27 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 28 | return err == nil 29 | } 30 | 31 | // Checks if an email is valid 32 | func IsValidEmail(email string) bool { 33 | _, err := mail.ParseAddress(email) 34 | return err == nil 35 | } 36 | -------------------------------------------------------------------------------- /repositories/auth.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mathvaillant/ticket-booking-project-v0/models" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type AuthRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | func (r *AuthRepository) RegisterUser(ctx context.Context, registerData *models.AuthCredentials) (*models.User, error) { 15 | user := &models.User{ 16 | Email: registerData.Email, 17 | Password: registerData.Password, 18 | } 19 | 20 | res := r.db.Model(&models.User{}).Create(user) 21 | 22 | if res.Error != nil { 23 | return nil, res.Error 24 | } 25 | 26 | return user, nil 27 | } 28 | 29 | func (r *AuthRepository) GetUser(ctx context.Context, query interface{}, args ...interface{}) (*models.User, error) { 30 | user := &models.User{} 31 | 32 | if res := r.db.Model(user).Where(query, args...).First(user); res.Error != nil { 33 | return nil, res.Error 34 | } 35 | 36 | return user, nil 37 | } 38 | 39 | func NewAuthRepository(db *gorm.DB) models.AuthRepository { 40 | return &AuthRepository{ 41 | db: db, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /models/ticket.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Ticket struct { 9 | ID uint `json:"id" gorm:"primarykey"` 10 | EventID uint `json:"eventId"` 11 | UserID uint `json:"userId" gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` 12 | Event Event `json:"event" gorm:"foreignkey:EventID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` 13 | Entered bool `json:"entered" default:"false"` 14 | CreatedAt time.Time `json:"createdAt"` 15 | UpdatedAt time.Time `json:"updatedAt"` 16 | } 17 | 18 | type TicketRepository interface { 19 | GetMany(ctx context.Context, userId uint) ([]*Ticket, error) 20 | GetOne(ctx context.Context, userId uint, ticketId uint) (*Ticket, error) 21 | CreateOne(ctx context.Context, userId uint, ticket *Ticket) (*Ticket, error) 22 | UpdateOne(ctx context.Context, userId uint, ticketId uint, updateData map[string]interface{}) (*Ticket, error) 23 | } 24 | 25 | type ValidateTicket struct { 26 | TicketId uint `json:"ticketId"` 27 | OwnerId uint `json:"ownerId"` 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | tty: true #keep the container running 6 | restart: always 7 | image: ticket-booking 8 | container_name: ticket-booking 9 | build: . 10 | ports: 11 | - 8081:8081 12 | env_file: 13 | - .env 14 | networks: 15 | - application 16 | depends_on: 17 | db: 18 | condition: service_healthy 19 | volumes: 20 | - .:/src/app 21 | command: air -c .air.toml 22 | 23 | db: 24 | image: postgres:alpine 25 | container_name: ticket-booking-db 26 | environment: 27 | - POSTGRES_HOST=${DB_HOST} 28 | - POSTGRES_DB=${DB_NAME} 29 | - POSTGRES_USER=${DB_USER} 30 | - POSTGRES_PASSWORD=${DB_PASSWORD} 31 | ports: 32 | - 5432:5432 33 | volumes: 34 | - postgres-db:/var/lib/postgresql/data 35 | networks: 36 | - application 37 | healthcheck: 38 | test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] 39 | interval: 10s 40 | timeout: 5s 41 | retries: 5 42 | 43 | networks: 44 | application: 45 | 46 | volumes: 47 | postgres-db: 48 | 49 | -------------------------------------------------------------------------------- /models/event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type Event struct { 11 | ID uint `json:"id" gorm:"primarykey"` 12 | Name string `json:"name"` 13 | Location string `json:"location"` 14 | TotalTicketsPurchased int64 `json:"totalTicketsPurchased" gorm:"-"` 15 | TotalTicketsEntered int64 `json:"totalTicketsEntered" gorm:"-"` 16 | Date time.Time `json:"date"` 17 | CreatedAt time.Time `json:"createdAt"` 18 | UpdatedAt time.Time `json:"updatedAt"` 19 | } 20 | 21 | type EventRepository interface { 22 | GetMany(ctx context.Context) ([]*Event, error) 23 | GetOne(ctx context.Context, eventId uint) (*Event, error) 24 | CreateOne(ctx context.Context, event *Event) (*Event, error) 25 | UpdateOne(ctx context.Context, eventId uint, updateData map[string]interface{}) (*Event, error) 26 | DeleteOne(ctx context.Context, eventId uint) error 27 | } 28 | 29 | func (e *Event) AfterFind(db *gorm.DB) (err error) { 30 | baseQuery := db.Model(&Ticket{}).Where(&Ticket{EventID: e.ID}) 31 | 32 | if res := baseQuery.Count(&e.TotalTicketsPurchased); res.Error != nil { 33 | return res.Error 34 | } 35 | if res := baseQuery.Where("entered = ?", true).Count(&e.TotalTicketsEntered); res.Error != nil { 36 | return res.Error 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/mathvaillant/ticket-booking-project-v0/config" 8 | "github.com/mathvaillant/ticket-booking-project-v0/db" 9 | "github.com/mathvaillant/ticket-booking-project-v0/handlers" 10 | "github.com/mathvaillant/ticket-booking-project-v0/middlewares" 11 | "github.com/mathvaillant/ticket-booking-project-v0/repositories" 12 | "github.com/mathvaillant/ticket-booking-project-v0/services" 13 | ) 14 | 15 | func main() { 16 | envConfig := config.NewEnvConfig() 17 | db := db.Init(envConfig, db.DBMigrator) 18 | 19 | app := fiber.New(fiber.Config{ 20 | AppName: "Ticket-Booking", 21 | ServerHeader: "Fiber", 22 | }) 23 | 24 | // Repositories 25 | eventRepository := repositories.NewEventRepository(db) 26 | ticketRepository := repositories.NewTicketRepository(db) 27 | authRepository := repositories.NewAuthRepository(db) 28 | 29 | // Service 30 | authService := services.NewAuthService(authRepository) 31 | 32 | // Routing 33 | server := app.Group("/api") 34 | handlers.NewAuthHandler(server.Group("/auth"), authService) 35 | 36 | privateRoutes := server.Use(middlewares.AuthProtected(db)) 37 | 38 | handlers.NewEventHandler(privateRoutes.Group("/event"), eventRepository) 39 | handlers.NewTicketHandler(privateRoutes.Group("/ticket"), ticketRepository) 40 | 41 | app.Listen(fmt.Sprintf(":" + envConfig.ServerPort)) 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mathvaillant/ticket-booking-project-v0 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/caarlos0/env v3.5.0+incompatible 7 | github.com/go-playground/validator/v10 v10.20.0 8 | github.com/gofiber/fiber/v2 v2.52.4 9 | github.com/golang-jwt/jwt/v5 v5.2.1 10 | github.com/joho/godotenv v1.5.1 11 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 12 | golang.org/x/crypto v0.19.0 13 | gorm.io/driver/postgres v1.5.7 14 | gorm.io/gorm v1.25.10 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.5 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 20 | github.com/go-playground/locales v0.14.1 // indirect 21 | github.com/go-playground/universal-translator v0.18.1 // indirect 22 | github.com/google/uuid v1.6.0 // indirect 23 | github.com/jackc/pgpassfile v1.0.0 // indirect 24 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 25 | github.com/jackc/pgx/v5 v5.4.3 // indirect 26 | github.com/jinzhu/inflection v1.0.0 // indirect 27 | github.com/jinzhu/now v1.1.5 // indirect 28 | github.com/klauspost/compress v1.17.0 // indirect 29 | github.com/leodido/go-urn v1.4.0 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/mattn/go-runewidth v0.0.15 // indirect 33 | github.com/rivo/uniseg v0.2.0 // indirect 34 | github.com/stretchr/testify v1.9.0 // indirect 35 | github.com/valyala/bytebufferpool v1.0.0 // indirect 36 | github.com/valyala/fasthttp v1.51.0 // indirect 37 | github.com/valyala/tcplisten v1.0.0 // indirect 38 | golang.org/x/net v0.21.0 // indirect 39 | golang.org/x/sys v0.17.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /repositories/ticket.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mathvaillant/ticket-booking-project-v0/models" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type TicketRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | func (r *TicketRepository) GetMany(ctx context.Context, userId uint) ([]*models.Ticket, error) { 15 | tickets := []*models.Ticket{} 16 | 17 | res := r.db.Model(&models.Ticket{}).Where("user_id = ?", userId).Preload("Event").Order("updated_at desc").Find(&tickets) 18 | 19 | if res.Error != nil { 20 | return nil, res.Error 21 | } 22 | 23 | return tickets, nil 24 | } 25 | 26 | func (r *TicketRepository) GetOne(ctx context.Context, userId uint, ticketId uint) (*models.Ticket, error) { 27 | ticket := &models.Ticket{} 28 | 29 | res := r.db.Model(ticket).Where("id = ?", ticketId).Where("user_id = ?", userId).Preload("Event").First(ticket) 30 | 31 | if res.Error != nil { 32 | return nil, res.Error 33 | } 34 | 35 | return ticket, nil 36 | } 37 | 38 | func (r *TicketRepository) CreateOne(ctx context.Context, userId uint, ticket *models.Ticket) (*models.Ticket, error) { 39 | ticket.UserID = userId 40 | 41 | res := r.db.Model(ticket).Create(ticket) 42 | 43 | if res.Error != nil { 44 | return nil, res.Error 45 | } 46 | 47 | return r.GetOne(ctx, userId, ticket.ID) 48 | } 49 | 50 | func (r *TicketRepository) UpdateOne(ctx context.Context, userId uint, ticketId uint, updateData map[string]interface{}) (*models.Ticket, error) { 51 | ticket := &models.Ticket{} 52 | 53 | updateRes := r.db.Model(ticket).Where("id = ?", ticketId).Updates(updateData) 54 | 55 | if updateRes.Error != nil { 56 | return nil, updateRes.Error 57 | } 58 | 59 | return r.GetOne(ctx, userId, ticketId) 60 | } 61 | 62 | func NewTicketRepository(db *gorm.DB) models.TicketRepository { 63 | return &TicketRepository{ 64 | db: db, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /repositories/event.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mathvaillant/ticket-booking-project-v0/models" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type EventRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | func (r *EventRepository) GetMany(ctx context.Context) ([]*models.Event, error) { 15 | events := []*models.Event{} 16 | 17 | res := r.db.Model(&models.Event{}).Order("updated_at desc").Find(&events) 18 | 19 | if res.Error != nil { 20 | return nil, res.Error 21 | } 22 | 23 | return events, nil 24 | } 25 | 26 | func (r *EventRepository) GetOne(ctx context.Context, eventId uint) (*models.Event, error) { 27 | event := &models.Event{} 28 | 29 | res := r.db.Model(event).Where("id = ?", eventId).First(event) 30 | 31 | if res.Error != nil { 32 | return nil, res.Error 33 | } 34 | 35 | return event, nil 36 | } 37 | 38 | func (r *EventRepository) CreateOne(ctx context.Context, event *models.Event) (*models.Event, error) { 39 | res := r.db.Model(event).Create(event) 40 | 41 | if res.Error != nil { 42 | return nil, res.Error 43 | } 44 | 45 | return event, nil 46 | } 47 | 48 | func (r *EventRepository) UpdateOne(ctx context.Context, eventId uint, updateData map[string]interface{}) (*models.Event, error) { 49 | event := &models.Event{} 50 | 51 | updateRes := r.db.Model(event).Where("id = ?", eventId).Updates(updateData) 52 | 53 | if updateRes.Error != nil { 54 | return nil, updateRes.Error 55 | } 56 | 57 | getRes := r.db.Model(event).Where("id = ?", eventId).First(event) 58 | 59 | if getRes.Error != nil { 60 | return nil, getRes.Error 61 | } 62 | 63 | return event, nil 64 | } 65 | 66 | func (r *EventRepository) DeleteOne(ctx context.Context, eventId uint) error { 67 | res := r.db.Delete(&models.Event{}, eventId) 68 | return res.Error 69 | } 70 | 71 | func NewEventRepository(db *gorm.DB) models.EventRepository { 72 | return &EventRepository{ 73 | db: db, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /middlewares/auth-protected.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/gofiber/fiber/v2" 10 | "github.com/gofiber/fiber/v2/log" 11 | "github.com/golang-jwt/jwt/v5" 12 | "github.com/mathvaillant/ticket-booking-project-v0/models" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func AuthProtected(db *gorm.DB) fiber.Handler { 17 | return func(ctx *fiber.Ctx) error { 18 | authHeader := ctx.Get("Authorization") 19 | 20 | if authHeader == "" { 21 | log.Warnf("empty authorization header") 22 | 23 | return ctx.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{ 24 | "status": "fail", 25 | "message": "Unauthorized", 26 | }) 27 | } 28 | 29 | tokenParts := strings.Split(authHeader, " ") 30 | 31 | if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { 32 | log.Warnf("invalid token parts") 33 | 34 | return ctx.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{ 35 | "status": "fail", 36 | "message": "Unauthorized", 37 | }) 38 | } 39 | 40 | tokenStr := tokenParts[1] 41 | secret := []byte(os.Getenv("JWT_SECRET")) 42 | 43 | token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { 44 | if token.Method.Alg() != jwt.GetSigningMethod("HS256").Alg() { 45 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 46 | } 47 | return secret, nil 48 | }) 49 | 50 | if err != nil || !token.Valid { 51 | log.Warnf("invalid token") 52 | 53 | return ctx.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{ 54 | "status": "fail", 55 | "message": "Unauthorized", 56 | }) 57 | } 58 | 59 | userId := token.Claims.(jwt.MapClaims)["id"] 60 | 61 | if err := db.Model(&models.User{}).Where("id = ?", userId).Error; errors.Is(err, gorm.ErrRecordNotFound) { 62 | log.Warnf("user not found in the db") 63 | 64 | return ctx.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{ 65 | "status": "fail", 66 | "message": "Unauthorized", 67 | }) 68 | } 69 | 70 | ctx.Locals("userId", userId) 71 | 72 | return ctx.Next() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services/auth.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/golang-jwt/jwt/v5" 11 | "github.com/mathvaillant/ticket-booking-project-v0/models" 12 | "github.com/mathvaillant/ticket-booking-project-v0/utils" 13 | "golang.org/x/crypto/bcrypt" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type AuthService struct { 18 | repository models.AuthRepository 19 | } 20 | 21 | func (s *AuthService) Login(ctx context.Context, loginData *models.AuthCredentials) (string, *models.User, error) { 22 | user, err := s.repository.GetUser(ctx, "email = ?", loginData.Email) 23 | 24 | if err != nil { 25 | if errors.Is(err, gorm.ErrRecordNotFound) { 26 | return "", nil, fmt.Errorf("invalid credentials") 27 | } 28 | return "", nil, err 29 | } 30 | 31 | if !models.MatchesHash(loginData.Password, user.Password) { 32 | return "", nil, fmt.Errorf("invalid credentials") 33 | } 34 | 35 | claims := jwt.MapClaims{ 36 | "id": user.ID, 37 | "role": user.Role, 38 | "exp": time.Now().Add(time.Hour * 168).Unix(), 39 | } 40 | 41 | token, err := utils.GenerateJWT(claims, jwt.SigningMethodHS256, os.Getenv("JWT_SECRET")) 42 | 43 | if err != nil { 44 | return "", nil, err 45 | } 46 | 47 | return token, user, nil 48 | } 49 | 50 | func (s *AuthService) Register(ctx context.Context, registerData *models.AuthCredentials) (string, *models.User, error) { 51 | if !models.IsValidEmail(registerData.Email) { 52 | return "", nil, fmt.Errorf("please, provide a valid email to register") 53 | } 54 | 55 | if _, err := s.repository.GetUser(ctx, "email = ?", registerData.Email); !errors.Is(err, gorm.ErrRecordNotFound) { 56 | return "", nil, fmt.Errorf("the user email is already in use") 57 | } 58 | 59 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(registerData.Password), bcrypt.DefaultCost) 60 | if err != nil { 61 | return "", nil, err 62 | } 63 | 64 | registerData.Password = string(hashedPassword) 65 | 66 | user, err := s.repository.RegisterUser(ctx, registerData) 67 | if err != nil { 68 | return "", nil, err 69 | } 70 | 71 | claims := jwt.MapClaims{ 72 | "id": user.ID, 73 | "role": user.Role, 74 | "exp": time.Now().Add(time.Hour * 168).Unix(), 75 | } 76 | 77 | // Generate the JWT 78 | token, err := utils.GenerateJWT(claims, jwt.SigningMethodHS256, os.Getenv("JWT_SECRET")) 79 | if err != nil { 80 | return "", nil, err 81 | } 82 | 83 | return token, user, nil 84 | } 85 | 86 | func NewAuthService(repository models.AuthRepository) models.AuthService { 87 | return &AuthService{ 88 | repository: repository, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format 2 | 3 | # Working directory 4 | # . or absolute path, please note that the directories following must be under root. 5 | root = "." 6 | tmp_dir = "tmp" 7 | 8 | [build] 9 | # Array of commands to run before each build 10 | pre_cmd = ["echo 'hello air' > pre_cmd.txt"] 11 | # Just plain old shell command. You could use `make` as well. 12 | cmd = "go build -o ./tmp/main ./cmd/api/main.go" 13 | # Array of commands to run after ^C 14 | post_cmd = ["echo 'hello air' > post_cmd.txt"] 15 | # Binary file yields from `cmd`. 16 | bin = "tmp/main" 17 | # Customize binary, can setup environment variables when run your app. 18 | full_bin = "APP_ENV=dev APP_USER=air ./tmp/main" 19 | # Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'. 20 | args_bin = ["hello", "world"] 21 | # Watch these filename extensions. 22 | include_ext = ["go", "tpl", "tmpl", "html"] 23 | # Ignore these filename extensions or directories. 24 | exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"] 25 | # Watch these directories if you specified. 26 | include_dir = [] 27 | # Watch these files. 28 | include_file = [] 29 | # Exclude files. 30 | exclude_file = [] 31 | # Exclude specific regular expressions. 32 | exclude_regex = ["_test\\.go"] 33 | # Exclude unchanged files. 34 | exclude_unchanged = true 35 | # Follow symlink for directories 36 | follow_symlink = true 37 | # This log file places in your tmp_dir. 38 | log = "air.log" 39 | # Poll files for changes instead of using fsnotify. 40 | poll = false 41 | # Poll interval (defaults to the minimum interval of 500ms). 42 | poll_interval = 500 # ms 43 | # It's not necessary to trigger build each time file changes if it's too frequent. 44 | delay = 0 # ms 45 | # Stop running old binary when build errors occur. 46 | stop_on_error = true 47 | # Send Interrupt signal before killing process (windows does not support this feature) 48 | send_interrupt = false 49 | # Delay after sending Interrupt signal 50 | kill_delay = 500 # nanosecond 51 | # Rerun binary or not 52 | rerun = false 53 | # Delay after each execution 54 | rerun_delay = 500 55 | 56 | [log] 57 | # Show log time 58 | time = false 59 | # Only show main log (silences watcher, build, runner) 60 | main_only = false 61 | 62 | [color] 63 | # Customize each part's color. If no color found, use the raw app log. 64 | main = "magenta" 65 | watcher = "cyan" 66 | build = "yellow" 67 | runner = "green" 68 | 69 | [misc] 70 | # Delete tmp directory on exit 71 | clean_on_exit = true 72 | 73 | [screen] 74 | clear_on_rebuild = true 75 | keep_scroll = true 76 | 77 | # Enable live-reloading on the browser. 78 | [proxy] 79 | enabled = true 80 | proxy_port = 8090 81 | app_port = 8080 82 | -------------------------------------------------------------------------------- /handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/gofiber/fiber/v2" 10 | "github.com/mathvaillant/ticket-booking-project-v0/models" 11 | ) 12 | 13 | var validate = validator.New() 14 | 15 | type AuthHandler struct { 16 | service models.AuthService 17 | } 18 | 19 | func (h *AuthHandler) Login(ctx *fiber.Ctx) error { 20 | creds := &models.AuthCredentials{} 21 | 22 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 23 | defer cancel() 24 | 25 | if err := ctx.BodyParser(&creds); err != nil { 26 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 27 | "status": "fail", 28 | "message": err.Error(), 29 | }) 30 | } 31 | 32 | if err := validate.Struct(creds); err != nil { 33 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 34 | "status": "fail", 35 | "message": err.Error(), 36 | }) 37 | } 38 | 39 | token, user, err := h.service.Login(context, creds) 40 | 41 | if err != nil { 42 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 43 | "status": "fail", 44 | "message": err.Error(), 45 | }) 46 | } 47 | 48 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 49 | "status": "success", 50 | "message": "Successfully logged in", 51 | "data": &fiber.Map{ 52 | "token": token, 53 | "user": user, 54 | }, 55 | }) 56 | } 57 | 58 | func (h *AuthHandler) Register(ctx *fiber.Ctx) error { 59 | creds := &models.AuthCredentials{} 60 | 61 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 62 | defer cancel() 63 | 64 | if err := ctx.BodyParser(&creds); err != nil { 65 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 66 | "status": "fail", 67 | "message": err.Error(), 68 | }) 69 | } 70 | 71 | if err := validate.Struct(creds); err != nil { 72 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 73 | "status": "fail", 74 | "message": fmt.Errorf("please, provide a valid name, email and password").Error(), 75 | }) 76 | } 77 | 78 | token, user, err := h.service.Register(context, creds) 79 | 80 | if err != nil { 81 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 82 | "status": "fail", 83 | "message": err.Error(), 84 | }) 85 | } 86 | 87 | return ctx.Status(fiber.StatusCreated).JSON(&fiber.Map{ 88 | "status": "success", 89 | "message": "Successfully registered", 90 | "data": &fiber.Map{ 91 | "token": token, 92 | "user": user, 93 | }, 94 | }) 95 | } 96 | 97 | func NewAuthHandler(route fiber.Router, service models.AuthService) { 98 | handler := &AuthHandler{ 99 | service: service, 100 | } 101 | 102 | route.Post("/login", handler.Login) 103 | route.Post("/register", handler.Register) 104 | } 105 | -------------------------------------------------------------------------------- /handlers/event.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/mathvaillant/ticket-booking-project-v0/models" 10 | ) 11 | 12 | type EventHandler struct { 13 | repository models.EventRepository 14 | } 15 | 16 | func (h *EventHandler) GetMany(ctx *fiber.Ctx) error { 17 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 18 | defer cancel() 19 | 20 | events, err := h.repository.GetMany(context) 21 | 22 | if err != nil { 23 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 24 | "status": "fail", 25 | "message": err.Error(), 26 | }) 27 | } 28 | 29 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 30 | "status": "success", 31 | "message": "", 32 | "data": events, 33 | }) 34 | } 35 | 36 | func (h *EventHandler) GetOne(ctx *fiber.Ctx) error { 37 | eventId, _ := strconv.Atoi(ctx.Params("eventId")) 38 | 39 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 40 | defer cancel() 41 | 42 | event, err := h.repository.GetOne(context, uint(eventId)) 43 | 44 | if err != nil { 45 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 46 | "status": "fail", 47 | "message": err.Error(), 48 | }) 49 | } 50 | 51 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 52 | "status": "success", 53 | "message": "", 54 | "data": event, 55 | }) 56 | } 57 | 58 | func (h *EventHandler) CreateOne(ctx *fiber.Ctx) error { 59 | event := &models.Event{} 60 | 61 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 62 | defer cancel() 63 | 64 | if err := ctx.BodyParser(event); err != nil { 65 | return ctx.Status(fiber.StatusUnprocessableEntity).JSON(&fiber.Map{ 66 | "status": "fail", 67 | "message": err.Error(), 68 | "data": nil, 69 | }) 70 | } 71 | 72 | event, err := h.repository.CreateOne(context, event) 73 | 74 | if err != nil { 75 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 76 | "status": "fail", 77 | "message": err.Error(), 78 | "data": nil, 79 | }) 80 | } 81 | 82 | return ctx.Status(fiber.StatusCreated).JSON(&fiber.Map{ 83 | "status": "success", 84 | "message": "Event created", 85 | "data": event, 86 | }) 87 | } 88 | 89 | func (h *EventHandler) UpdateOne(ctx *fiber.Ctx) error { 90 | eventId, _ := strconv.Atoi(ctx.Params("eventId")) 91 | updateData := make(map[string]interface{}) 92 | 93 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 94 | defer cancel() 95 | 96 | if err := ctx.BodyParser(&updateData); err != nil { 97 | return ctx.Status(fiber.StatusUnprocessableEntity).JSON(&fiber.Map{ 98 | "status": "fail", 99 | "message": err.Error(), 100 | "data": nil, 101 | }) 102 | } 103 | 104 | event, err := h.repository.UpdateOne(context, uint(eventId), updateData) 105 | 106 | if err != nil { 107 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 108 | "status": "fail", 109 | "message": err.Error(), 110 | "data": nil, 111 | }) 112 | } 113 | 114 | return ctx.Status(fiber.StatusCreated).JSON(&fiber.Map{ 115 | "status": "success", 116 | "message": "Event updated", 117 | "data": event, 118 | }) 119 | } 120 | 121 | func (h *EventHandler) DeleteOne(ctx *fiber.Ctx) error { 122 | eventId, _ := strconv.Atoi(ctx.Params("eventId")) 123 | 124 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 125 | defer cancel() 126 | 127 | err := h.repository.DeleteOne(context, uint(eventId)) 128 | 129 | if err != nil { 130 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 131 | "status": "fail", 132 | "message": err.Error(), 133 | }) 134 | } 135 | 136 | return ctx.SendStatus(fiber.StatusNoContent) 137 | } 138 | 139 | func NewEventHandler(router fiber.Router, repository models.EventRepository) { 140 | handler := &EventHandler{ 141 | repository: repository, 142 | } 143 | 144 | router.Get("/", handler.GetMany) 145 | router.Post("/", handler.CreateOne) 146 | router.Get("/:eventId", handler.GetOne) 147 | router.Put("/:eventId", handler.UpdateOne) 148 | router.Delete("/:eventId", handler.DeleteOne) 149 | } 150 | -------------------------------------------------------------------------------- /handlers/ticket.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gofiber/fiber/v2" 10 | "github.com/mathvaillant/ticket-booking-project-v0/models" 11 | "github.com/skip2/go-qrcode" 12 | ) 13 | 14 | type TicketHandler struct { 15 | repository models.TicketRepository 16 | } 17 | 18 | func (h *TicketHandler) GetMany(ctx *fiber.Ctx) error { 19 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 20 | defer cancel() 21 | 22 | userId := uint(ctx.Locals("userId").(float64)) 23 | 24 | tickets, err := h.repository.GetMany(context, userId) 25 | 26 | if err != nil { 27 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 28 | "status": "fail", 29 | "message": err.Error(), 30 | }) 31 | } 32 | 33 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 34 | "status": "success", 35 | "message": "", 36 | "data": tickets, 37 | }) 38 | } 39 | 40 | func (h *TicketHandler) GetOne(ctx *fiber.Ctx) error { 41 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 42 | defer cancel() 43 | 44 | ticketId, _ := strconv.Atoi(ctx.Params("ticketId")) 45 | userId := uint(ctx.Locals("userId").(float64)) 46 | 47 | ticket, err := h.repository.GetOne(context, userId, uint(ticketId)) 48 | 49 | if err != nil { 50 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 51 | "status": "fail", 52 | "message": err.Error(), 53 | }) 54 | } 55 | 56 | var QRCode []byte 57 | QRCode, err = qrcode.Encode( 58 | fmt.Sprintf("ticketId:%v,ownerId:%v", ticketId, userId), 59 | qrcode.Medium, 60 | 256, 61 | ) 62 | 63 | if err != nil { 64 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 65 | "status": "fail", 66 | "message": err.Error(), 67 | }) 68 | } 69 | 70 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 71 | "status": "success", 72 | "message": "", 73 | "data": &fiber.Map{ 74 | "ticket": ticket, 75 | "qrcode": QRCode, 76 | }, 77 | }) 78 | } 79 | 80 | func (h *TicketHandler) CreateOne(ctx *fiber.Ctx) error { 81 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 82 | defer cancel() 83 | 84 | ticket := &models.Ticket{} 85 | userId := uint(ctx.Locals("userId").(float64)) 86 | 87 | if err := ctx.BodyParser(ticket); err != nil { 88 | return ctx.Status(fiber.StatusUnprocessableEntity).JSON(&fiber.Map{ 89 | "status": "fail", 90 | "message": err.Error(), 91 | "data": nil, 92 | }) 93 | } 94 | 95 | ticket, err := h.repository.CreateOne(context, userId, ticket) 96 | 97 | if err != nil { 98 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 99 | "status": "fail", 100 | "message": err.Error(), 101 | "data": nil, 102 | }) 103 | } 104 | 105 | return ctx.Status(fiber.StatusCreated).JSON(&fiber.Map{ 106 | "status": "success", 107 | "message": "Ticket created", 108 | "data": ticket, 109 | }) 110 | } 111 | 112 | func (h *TicketHandler) ValidateOne(ctx *fiber.Ctx) error { 113 | context, cancel := context.WithTimeout(context.Background(), time.Duration(5*time.Second)) 114 | defer cancel() 115 | 116 | validateBody := &models.ValidateTicket{} 117 | 118 | if err := ctx.BodyParser(validateBody); err != nil { 119 | return ctx.Status(fiber.StatusUnprocessableEntity).JSON(&fiber.Map{ 120 | "status": "fail", 121 | "message": err.Error(), 122 | "data": nil, 123 | }) 124 | } 125 | 126 | validateData := make(map[string]interface{}) 127 | validateData["entered"] = true 128 | 129 | ticket, err := h.repository.UpdateOne(context, validateBody.OwnerId, validateBody.TicketId, validateData) 130 | 131 | if err != nil { 132 | return ctx.Status(fiber.StatusBadRequest).JSON(&fiber.Map{ 133 | "status": "fail", 134 | "message": err.Error(), 135 | "data": nil, 136 | }) 137 | } 138 | 139 | return ctx.Status(fiber.StatusOK).JSON(&fiber.Map{ 140 | "status": "success", 141 | "message": "Welcome to the show!", 142 | "data": ticket, 143 | }) 144 | } 145 | 146 | func NewTicketHandler(router fiber.Router, repository models.TicketRepository) { 147 | handler := &TicketHandler{ 148 | repository: repository, 149 | } 150 | 151 | router.Get("/", handler.GetMany) 152 | router.Post("/", handler.CreateOne) 153 | router.Get("/:ticketId", handler.GetOne) 154 | router.Post("/validate", handler.ValidateOne) 155 | } 156 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= 4 | github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 9 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 10 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 11 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 12 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 13 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 14 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 15 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 16 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 17 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 18 | github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= 19 | github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 20 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 21 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 25 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 26 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 27 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 28 | github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= 29 | github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 30 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 31 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 32 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 33 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 34 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 35 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 36 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 37 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 38 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 39 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 40 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 41 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 42 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 43 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 44 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 45 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 46 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 50 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 51 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 52 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 57 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 58 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 59 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 60 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 61 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 62 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 63 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 64 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 65 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 66 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 67 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 68 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 71 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 72 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 73 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 75 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 77 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= 79 | gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= 80 | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= 81 | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 82 | --------------------------------------------------------------------------------