diff --git a/.env.example b/.env.example index bf52b32..c969e47 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,14 @@ DB_HOST= DB_PORT= DB_NAME= +SMTP_HOST= +SMTP_USER= +SMTP_PASS= + +ALLOW_DATABASE_SEEDING=false APP_FRONTEND_URL="http://localhost:5173" JWT_SECRET_KEY="example_key" + +VEHICLE_1_NAME="Freenove 1" +VEHICLE_1_IPADDRESS="0.0.0.0" +VEHICLE_1_IS_AVAILABLE=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d235417 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.21.6 as builder + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN go build -o main . + +FROM golang:1.21.6 + +WORKDIR /app + +COPY --from=builder /app/main . + +COPY --from=builder /app/src ./src + +EXPOSE 8000 + +CMD ["./main"] diff --git a/docker-compose.yml b/docker-compose.yml index 9233231..a8bd9aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,23 @@ -services: +version: '3.9' + +services: api: - build: + build: context: . dockerfile: Dockerfile_dev container_name: go environment: - Port: 8000 + - PORT=8000 + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT} + - DB_NAME=${DB_NAME} + - DB_USER=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - SMTP_HOST=${SMTP_HOST} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${SMTP_USER} + - TEMPLATE_PATH=/app/src/templates ports: - "8000:8000" volumes: @@ -16,28 +28,36 @@ services: depends_on: mysql: condition: service_healthy + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s mosquitto: image: eclipse-mosquitto + container_name: mosquitto ports: - - "1883:1883" - - "9001:9001" + - "1883:1883" + - "9001:9001" volumes: - - ./mosquitto.conf:/mosquitto/config/mosquitto.conf + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf restart: always networks: - goland-network mysql: container_name: mysql + image: mysql:latest environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: database healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + test: [ "CMD", "mysql", "-uroot", "-proot", "-e", "SELECT 1" ] + interval: 5s timeout: 2s - retries: 10 - image: mysql:latest + retries: 3 ports: - "3306:3306" restart: always @@ -48,17 +68,27 @@ services: adminer: container_name: adminer + image: adminer depends_on: - mysql - image: adminer ports: - - 8080:8080 + - "8080:8080" restart: always networks: - goland-network + email_service: + image: bytemark/smtp + container_name: smtp + environment: + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + ports: + - "1025:25" + restart: always + networks: + - goland-network networks: goland-network: driver: bridge - \ No newline at end of file diff --git a/documentation/golang-api.postman_collection.json b/documentation/golang-api.postman_collection.json index 0e0f9a9..539f067 100644 --- a/documentation/golang-api.postman_collection.json +++ b/documentation/golang-api.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "8e9963c4-5424-4358-acd1-e98127500ef2", + "_postman_id": "0e01642e-03df-473f-9066-2df4319b8c75", "name": "golang-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "21427084", - "_collection_link": "https://hetic-project.postman.co/workspace/projet-restaurant-golang~aaccd814-8832-4e52-a0ee-6234c27c4a66/collection/21427084-8e9963c4-5424-4358-acd1-e98127500ef2?action=share&source=collection_link&creator=21427084" + "_collection_link": "https://hetic-project.postman.co/workspace/projet-restaurant-golang~aaccd814-8832-4e52-a0ee-6234c27c4a66/collection/21427084-0e01642e-03df-473f-9066-2df4319b8c75?action=share&source=collection_link&creator=21427084" }, "item": [ { @@ -74,6 +74,77 @@ } }, "response": [] + } + ] + }, + { + "name": "Race", + "item": [ + { + "name": "Index", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{bearerToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/api/races", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "api", + "races" + ] + } + }, + "response": [] + }, + { + "name": "Index user races", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{bearerToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/api/users/:userId/races", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "api", + "users", + ":userId", + "races" + ], + "variable": [ + { + "key": "userId", + "value": "4" + } + ] + } + }, + "response": [] }, { "name": "Post", @@ -92,7 +163,7 @@ "header": [], "body": { "mode": "raw", - "raw": "", + "raw": "{\n \"start_time\": \"2024-08-25 19:07:00\",\n \"type\": \"manual\",\n \"name\": \"ma course\",\n \"status\": \"not_started\",\n \"average_speed\": 0.13,\n \"vehicle_id\": 1\n}", "options": { "raw": { "language": "json" @@ -100,14 +171,22 @@ } }, "url": { - "raw": "localhost:8000/api/vehicles", + "raw": "localhost:8000/api/users/:userId/races", "host": [ "localhost" ], "port": "8000", "path": [ "api", - "vehicles" + "users", + ":userId", + "races" + ], + "variable": [ + { + "key": "userId", + "value": "1" + } ] } }, @@ -130,7 +209,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \n}", + "raw": "{\n \"end_time\": \"2024-08-25 19:09:00\",\n \"status\": \"completed\"\n}", "options": { "raw": { "language": "json" @@ -138,14 +217,21 @@ } }, "url": { - "raw": "localhost:8000/api/vehicles", + "raw": "localhost:8000/api/races/:raceId", "host": [ "localhost" ], "port": "8000", "path": [ "api", - "vehicles" + "races", + ":raceId" + ], + "variable": [ + { + "key": "raceId", + "value": "15" + } ] } }, @@ -167,20 +253,100 @@ "method": "DELETE", "header": [], "url": { - "raw": "localhost:8000/api/vehicles/:id", + "raw": "localhost:8000/api/races/5", "host": [ "localhost" ], "port": "8000", "path": [ "api", - "vehicles", + "races", + "5" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Vehicle state", + "item": [ + { + "name": "get a vehicle state", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{bearerToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/api/vehicle-states/:id", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "api", + "vehicle-states", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "patch a vehicle state", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{bearerToken}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"face\": 0,\n \"led_animation\": 0,\n \"buzzer_alarm\": 0,\n \"video_activated\": 0,\n \"PrimaryLedColor\": {\n \"led_identifier\": 0,\n \"red\": 0,\n \"green\": 0,\n \"blue\": 0\n },\n \"SecondaryLedColor\": {\n \"binary_representation\": 0,\n \"red\": 0,\n \"green\": 0,\n \"blue\": 0\n },\n \"BuzzerVariable\": {\n \"activated\": 0,\n \"frequency\": 0\n },\n \"HeadAngle\": {\n \"vertical_angle\": 0,\n \"horizontal_angle\": 0\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/api/vehicle-states/:id", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "api", + "vehicle-states", ":id" ], "variable": [ { "key": "id", - "value": "" + "value": "1" } ] } diff --git a/go.mod b/go.mod index 0ec1f4a..5a9a5db 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.25.0 + gopkg.in/mail.v2 v2.3.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 diff --git a/go.sum b/go.sum index 557ed21..11e8a84 100644 --- a/go.sum +++ b/go.sum @@ -110,9 +110,13 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index fb4b07a..c4d191f 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,20 @@ package main import ( + "api/src/config" "api/src/models" + "api/src/models/attributes" "api/src/routes" "api/src/services" "fmt" "log" + "os" + "strconv" + "strings" + "time" "github.com/gin-gonic/gin" + "github.com/joho/godotenv" "gorm.io/gorm" ) @@ -20,6 +27,10 @@ func main() { performMigrations(database) + initVehicleData(database) + + seedDatabase(database) + initMQTT() startWebServer() @@ -47,7 +58,21 @@ func closeDatabaseConnection(database *gorm.DB) { connection.Close() - err = database.AutoMigrate(&models.User{}, &models.SensorData{}, &models.Vehicle{}, &models.Fool{}, &models.Race{}) + fmt.Println("Database migrations completed.") +} + +func performMigrations(database *gorm.DB) { + err := database.AutoMigrate( + &models.User{}, + &models.Vehicle{}, + &models.Race{}, + &models.VehicleState{}, + &models.PrimaryLedColor{}, + &models.SecondaryLedColor{}, + &models.BuzzerVariable{}, + &models.HeadAngle{}, + &models.VehicleBattery{}, + ) if err != nil { log.Fatal("Error performing database migrations: ", err) @@ -56,18 +81,127 @@ func closeDatabaseConnection(database *gorm.DB) { fmt.Println("Database migrations completed.") } -func performMigrations(database *gorm.DB) { - err := database.AutoMigrate(&models.User{}, &models.SensorData{}, &models.Vehicle{}, &models.Fool{}, &models.Race{}) +func initVehicleData(database *gorm.DB) { + err := godotenv.Load() if err != nil { - log.Fatal("Error performing database migrations: ", err) + log.Fatal("Error loading .env file in initVehicleData: ", err) } - fmt.Println("Database migrations completed.") + for i := 1; ; i++ { + nameKey := fmt.Sprintf("VEHICLE_%d_NAME", i) + ipAddressKey := fmt.Sprintf("VEHICLE_%d_IPADDRESS", i) + availableKey := fmt.Sprintf("VEHICLE_%d_IS_AVAILABLE", i) + + name := os.Getenv(nameKey) + ip := os.Getenv(ipAddressKey) + isAvailableStr := os.Getenv(availableKey) + + if name == "" || ip == "" || isAvailableStr == "" { + break + } + + isAvailable, err := strconv.ParseBool(strings.TrimSpace(isAvailableStr)) + + if err != nil { + log.Fatal("Error parsing isAvailable variable to boolean in initVehicleData: ", err) + } + + database.FirstOrCreate(&models.Vehicle{ + Name: name, + IpAdress: ip, + IsAvailable: isAvailable, + }) + } +} + +func seedDatabase(database *gorm.DB) { + err := godotenv.Load() + + if err != nil { + log.Fatal("Error loading .env file in seedDatabase: ", err) + } + + allowDatabaseSeeding := os.Getenv("ALLOW_DATABASE_SEEDING") + if allowDatabaseSeeding != "true" { + return + } + + var usernames = []string{"David", "Goliath", "Pierre"} + var emails = []string{"l-david@test.com", "a-goliath@test.com", "q-pierre@test.com"} + hashedPassword, _ := services.HashPassword("password") + + for i := 0; i < len(usernames); i++ { + database.FirstOrCreate(&models.User{}, models.User{ + Username: &usernames[i], + Email: emails[i], + Password: string(hashedPassword), + }) + } + + var vehicle models.Vehicle + database.FirstOrCreate(&vehicle, models.Vehicle{ + Name: "Seed Vehicle", + IpAdress: "0.0.0.0", + IsAvailable: false, + }) + + var users []models.User + var vehicles []models.Vehicle + var races []models.Race + var userAlreadyHaveRaces bool + database.Where("username IN ?", usernames).Find(&users) + database.Find(&vehicles) + + for _, user := range users { + var userAlreadyHaveVehicleStateForThisVehicle bool + + for _, vehicle := range vehicles { + userAlreadyHaveVehicleStateForThisVehicle = database.Where("user_id = ? AND vehicle_id = ?", user.ID, vehicle.ID). + Find(&models.VehicleState{}). + RowsAffected > 0 + + if !userAlreadyHaveVehicleStateForThisVehicle { + vehicle.InitVehicleState(&user, database) + } + } + + userAlreadyHaveRaces = database.Where("user_id = ?", user.ID).Find(&races).RowsAffected >= 3 + + if userAlreadyHaveRaces { + continue + } + + raceNames := []string{"Morning Sprint", "Afternoon Challenge", "Evening Marathon"} + + for i := 0; i < 3; i++ { + var startTime attributes.CustomTime + startTime.Time = time.Now().Add(time.Duration(i) * time.Hour) + + endTime := &attributes.CustomTime{} + + endTime.Time = startTime.Time.Add(time.Duration(i) * time.Minute) + + race := models.Race{ + VehicleID: vehicle.ID, + StartTime: startTime, + EndTime: endTime, + CollisionDuration: 0, + DistanceCovered: 100 + (i * 10), + AverageSpeed: float64(i), + OutOfParcours: 0, + UserID: user.ID, + Type: "manual", + Status: "completed", + Name: raceNames[i], + } + database.Create(&race) + } + } } func initMQTT() { - fmt.Println("Starting mqtt connection...") + fmt.Println("Starting MQTT connection...") client := services.InitMQTTClient("tcp://mosquitto:1883") @@ -77,13 +211,20 @@ func initMQTT() { } func startWebServer() { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + router := gin.New() router.Use(gin.Logger()) router.Use(gin.Recovery()) - router = routes.SetupRouter(router) + router = routes.SetupRouter(router, cfg) - router.Run(":8000") + if err := router.Run(":8000"); err != nil { + log.Fatalf("Failed to start server: %v", err) + } fmt.Println("Server started on port 8000.") } diff --git a/readme.md b/readme.md index e50f7c1..6aa89ec 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,10 @@ Log into the GO container ``` ## Testing +Run api login test without docker +```bash + go test ./src/tests/auth/login -v +``` Run api tests ```bash diff --git a/src/actions/insertSensorDataAction.go b/src/actions/insertSensorDataAction.go index 196c99f..0f3a37f 100644 --- a/src/actions/insertSensorDataAction.go +++ b/src/actions/insertSensorDataAction.go @@ -1,5 +1 @@ package actions - -func insert() { - -} \ No newline at end of file diff --git a/src/config/config.go b/src/config/config.go new file mode 100644 index 0000000..59f3d03 --- /dev/null +++ b/src/config/config.go @@ -0,0 +1,31 @@ +package config + +import ( + "os" +) + +type Config struct { + DbHost string + DbPort string + DbName string + DbUser string + DbPassword string + SmtpHost string + SmtpUser string + SmtpPass string + EmailFrom string +} + +func LoadConfig() (*Config, error) { + return &Config{ + DbHost: os.Getenv("DB_HOST"), + DbPort: os.Getenv("DB_PORT"), + DbName: os.Getenv("DB_NAME"), + DbUser: os.Getenv("DB_USERNAME"), + DbPassword: os.Getenv("DB_PASSWORD"), + SmtpHost: os.Getenv("SMTP_HOST"), + SmtpUser: os.Getenv("SMTP_USER"), + SmtpPass: os.Getenv("SMTP_PASS"), + EmailFrom: os.Getenv("SMTP_USER"), + }, nil +} diff --git a/src/controllers/authController.go b/src/controllers/authController.go index d51e580..45a29c0 100644 --- a/src/controllers/authController.go +++ b/src/controllers/authController.go @@ -2,21 +2,24 @@ package controllers import ( "api/src/models" + "api/src/models/attributes" "api/src/services" + "errors" "net/http" "time" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" ) var jwtKey = []byte("my_secret_key") type RegisterCredentials struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` + Username *string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` } type LoginCredentials struct { @@ -30,61 +33,141 @@ type Claims struct { } type UserInfo struct { - ID uint `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + CreatedAt attributes.CustomTime `json:"created_at"` + UpdatedAt attributes.CustomTime `json:"updated_at"` } func Signup(c *gin.Context) { var creds RegisterCredentials if err := c.BindJSON(&creds); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + services.SetJsonBindingErrorResponse(c, err) return } - hashedPassword, err := services.HashPassword(creds.Password) + if (creds.Email == "") || (creds.Password == "") { + services.SetUnprocessableEntity(c, "Email address and password are required") + return + } - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + if !services.EmailValidator(creds.Email) { + services.SetUnprocessableEntity(c, "User email address is invalid") return } - user := models.User{Email: creds.Email, Password: string(hashedPassword), Username: creds.Username} + connection := services.GetConnection() + + if connection.Where("email = ?", creds.Email).First(&models.User{}).RowsAffected > 0 { + services.SetUnprocessableEntity(c, "A user with this email address already exists") + return + } - result := services.GetConnection().Create(&user) + if creds.Username != nil && connection.Where("username = ?", *creds.Username).First(&models.User{}).RowsAffected > 0 { + services.SetUnprocessableEntity(c, "A user with this username already exists") + return + } - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + hashedPassword, err := services.HashPassword(creds.Password) + if err != nil { + services.SetInternalServerError(c, "Internal server error while hashing password") return } - c.JSON(http.StatusCreated, gin.H{"message": "User created"}) + var user models.User + var errorOccuredInTransaction error + + connection.Transaction(func(transaction *gorm.DB) error { + user = models.User{ + Username: creds.Username, + Email: creds.Email, + Password: string(hashedPassword), + } + + result := transaction.Create(&user) + if result.Error != nil { + errorOccuredInTransaction = result.Error + return errorOccuredInTransaction + } + + vehicles := []models.Vehicle{} + transaction.Find(&vehicles) + + if len(vehicles) == 0 { + errorOccuredInTransaction = errors.New( + "cannot register a user because no vehicles are available in the database", + ) + return errorOccuredInTransaction + } + + for _, vehicle := range vehicles { + _, createVehiculeStateError := vehicle.InitVehicleState(&user, transaction) + if createVehiculeStateError != nil { + errorOccuredInTransaction = errors.New( + createVehiculeStateError.Error(), + ) + return createVehiculeStateError + } + } + return nil + }) + + if errorOccuredInTransaction != nil { + services.SetInternalServerError(c, errorOccuredInTransaction.Error()) + return + } + + c.Set("user_email", user.Email) + if user.Username != nil { + c.Set("username", *user.Username) + } + + services.SetCreated(c, "User created", user) } func Login(c *gin.Context) { var creds LoginCredentials if err := c.BindJSON(&creds); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + services.SetJsonBindingErrorResponse(c, err) return } - var user models.User + rememberMe := c.DefaultPostForm("remember_me", "false") == "true" - if err := services.GetConnection().Where("email = ?", creds.Email).First(&user).Error; err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + if (creds.Email == "") || (creds.Password == "") { + services.SetUnprocessableEntity(c, "Email address and password are required") + return + } + + if !services.EmailValidator(creds.Email) { + services.SetUnprocessableEntity(c, "User email address is invalid") + return + } + + var user models.User + query := services.GetConnection().Where("email = ?", creds.Email).Find(&user) + + if query.Error != nil { + if errors.Is(query.Error, gorm.ErrRecordNotFound) { + services.SetNotFound(c, "Invalid credentials") + return + } + services.SetInternalServerError(c, "Internal server error") return } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(creds.Password)); err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + services.SetUnprocessableEntity(c, "Invalid credentials") return } expirationTime := time.Now().Add(3 * time.Hour) + if rememberMe { + expirationTime = time.Now().Add(24 * time.Hour) + } claims := &Claims{ Email: creds.Email, @@ -96,16 +179,15 @@ func Login(c *gin.Context) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(jwtKey) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + services.SetInternalServerError(c, "Internal server error") return } userInfo := UserInfo{ ID: user.ID, Email: user.Email, - Username: user.Username, + Username: *user.Username, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, } @@ -121,7 +203,7 @@ func Welcome(c *gin.Context) { claims, err := GetClaimsFromToken(c) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + services.SetUnauthorized(c, "Unauthorized") return } diff --git a/src/controllers/fool/deleteFoolHandler.go b/src/controllers/fool/deleteFoolHandler.go deleted file mode 100644 index a955950..0000000 --- a/src/controllers/fool/deleteFoolHandler.go +++ /dev/null @@ -1,34 +0,0 @@ -package handlers - -import ( - "fmt" - - "api/src/models" - "api/src/services" - - "github.com/gin-gonic/gin" -) - -func DeleteFoolHandler(c *gin.Context) { - foolId := c.Param("foolId") - - // Access the database connection - db := services.GetConnection() - - var existingFool models.Fool - if err := db.First(&existingFool, foolId).Error; err != nil { - fmt.Println("Error retrieving fool from the database:", err) - services.SetNotFound(c, "Fool not found") - return - } - - fmt.Println("Existing fool to delete:", existingFool) - - if err := db.Delete(&existingFool).Error; err != nil { - fmt.Println("Error deleting fool from the database:", err) - services.SetInternalServerError(c, "Failed to delete fool") - return - } - - services.SetNoContent(c) -} diff --git a/src/controllers/race/createRaceHandler.go b/src/controllers/race/createRaceHandler.go index 7562121..b0bcc07 100644 --- a/src/controllers/race/createRaceHandler.go +++ b/src/controllers/race/createRaceHandler.go @@ -2,50 +2,79 @@ package handlers import ( "api/src/models" + "api/src/models/attributes" "api/src/services" validators "api/src/validators/race" - "fmt" + "strconv" "github.com/gin-gonic/gin" ) func CreateRaceHandler(c *gin.Context) { - var race models.Race + // Récupération de l'ID utilisateur depuis les paramètres de l'URL + userIdStr := c.Param("userId") + if userIdStr == "" || userIdStr == "0" || userIdStr == ":userId" { + services.SetUnprocessableEntity(c, "User not found") + return + } + // Conversion du userId de string à uint + userId, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + services.SetUnprocessableEntity(c, "Invalid user ID") + return + } - if err := c.ShouldBindJSON(&race); err != nil { - services.SetJsonBindingErrorResponse(c, err) + var createRaceValidator validators.CreateRaceValidator + // Validation de la requête JSON + if err := c.ShouldBindJSON(&createRaceValidator); err != nil { + services.SetJsonBindingErrorResponse(c, err) return } - raceValidator := validators.CreateRaceValidator{ - Duration: race.Duration, - ElapsedTime: race.ElapsedTime, - Laps: race.Laps, - RaceType: race.RaceType, - AverageSpeed: race.AverageSpeed, - TotalFaults: race.TotalFaults, - EffectiveDuration: race.EffectiveDuration, - UserID: race.UserID, - VehicleID: race.VehicleID, + // Validation des données via le validateur + if err := createRaceValidator.Validate(); err != nil { + services.SetValidationErrorResponse(c, err) + return } - if err := raceValidator.Validate(); err != nil { - services.SetValidationErrorResponse(c, err) + db := services.GetConnection() + if db.Where("id = ?", userId).Find(&models.User{}).RowsAffected == 0 { + services.SetUnprocessableEntity(c, "User not found") return } - db := services.GetConnection() + startTime := createRaceValidator.StartTime + var endTime *attributes.CustomTime - if err := db.Create(&race).Error; err != nil { - // Log the detailed error message for debugging purposes - fmt.Printf("Error creating Race: %v\n", err) + if createRaceValidator.EndTime != nil { + endTime = createRaceValidator.EndTime + } - // Set an appropriate error response - services.SetInternalServerError(c, "Failed to create Race") + // Création du modèle Race avec les données validées + race := models.Race{ + Name: createRaceValidator.Name, + StartTime: startTime, + EndTime: endTime, + CollisionDuration: 0, + DistanceCovered: 0, + AverageSpeed: 0, + OutOfParcours: 0, + Status: createRaceValidator.Status, + Type: createRaceValidator.Type, + VehicleID: createRaceValidator.VehicleID, + UserID: uint(userId), + } + + // Création de l'enregistrement dans la base de données + if err := db.Create(&race).Error; err != nil { + services.SetInternalServerError(c, err.Error()) return } + db.Preload("Vehicle").Find(&race) + + // Réponse de succès avec l'objet Race créé services.SetCreated(c, "Race created successfully", race) } diff --git a/src/controllers/race/deleteRaceHandler.go b/src/controllers/race/deleteRaceHandler.go index 9b98988..b29df3f 100644 --- a/src/controllers/race/deleteRaceHandler.go +++ b/src/controllers/race/deleteRaceHandler.go @@ -1,8 +1,6 @@ package handlers import ( - "fmt" - "api/src/models" "api/src/services" @@ -10,23 +8,22 @@ import ( ) func DeleteRaceHandler(c *gin.Context) { - raceId := c.Param("raceId") + raceID := c.Param("raceId") // Access the database connection db := services.GetConnection() var existingRace models.Race - if err := db.First(&existingRace, raceId).Error; err != nil { - fmt.Println("Error retrieving race from the database:", err) + + // Recherche de la course par ID + if err := db.Where("id = ?", raceID).First(&existingRace).Error; err != nil { services.SetNotFound(c, "Race not found") return } - fmt.Println("Existing race to delete:", existingRace) - + // Suppression de la course if err := db.Delete(&existingRace).Error; err != nil { - fmt.Println("Error deleting race from the database:", err) - services.SetInternalServerError(c, "Failed to delete race") + services.SetInternalServerError(c, "Internal server error") return } diff --git a/src/controllers/race/getAllUsersRaceHandler.go b/src/controllers/race/getAllUsersRaceHandler.go new file mode 100644 index 0000000..b164197 --- /dev/null +++ b/src/controllers/race/getAllUsersRaceHandler.go @@ -0,0 +1,28 @@ +package handlers + +import ( + "api/src/models" + "api/src/services" + "strconv" + + "github.com/gin-gonic/gin" +) + +func GetAllUsersRaceHandler(c *gin.Context) { + db := services.GetConnection() + + var races []models.Race + + userIdStr := c.Param("userId") + + // Convertir userId de string à uint + userId, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + services.SetUnprocessableEntity(c, "Invalid user ID") + return + } + + db.Where("user_id = ?", uint(userId)).Preload("Vehicle").Find(&races) + + services.SetOK(c, "User races retrieved successfully", races) +} diff --git a/src/controllers/race/getSingleRaceHandler.go b/src/controllers/race/getSingleRaceHandler.go deleted file mode 100644 index 9442eaf..0000000 --- a/src/controllers/race/getSingleRaceHandler.go +++ /dev/null @@ -1,27 +0,0 @@ -package handlers - -import ( - "api/src/models" - "api/src/services" - - "github.com/gin-gonic/gin" -) - -func GetSingleRaceHandler(c *gin.Context) { - db := services.GetConnection() - - var race models.Race - - raceId := c.Param("raceId") - - if err := db.Preload("Vehicle").First(&race, raceId).Error; err != nil { - if err.Error() == "record not found" { - services.SetNotFound(c, "Race not found") - } else { - services.SetInternalServerError(c, "Failed to retrieve race") - } - return - } - - services.SetOK(c, "Race retrieved successfully", race) -} diff --git a/src/controllers/race/updateRaceHandler.go b/src/controllers/race/updateRaceHandler.go index 3c1139d..92b21fe 100644 --- a/src/controllers/race/updateRaceHandler.go +++ b/src/controllers/race/updateRaceHandler.go @@ -1,11 +1,10 @@ package handlers import ( - "fmt" - "api/src/models" "api/src/services" validators "api/src/validators/race" + "fmt" "github.com/gin-gonic/gin" ) @@ -15,25 +14,25 @@ func UpdateRaceHandler(c *gin.Context) { db := services.GetConnection() + // Récupère la course existante var existingRace models.Race - if err := db.Preload("Vehicle").First(&existingRace, raceID).Error; err != nil { - fmt.Println("Error retrieving Race from the database:", err) - services.SetNotFound(c, "Race not found") + + if err := db.First(&existingRace, raceID).Error; err != nil { + if err.Error() == "record not found" { + services.SetNotFound(c, err.Error()) + return + } + + services.SetInternalServerError(c, err.Error()) return } - fmt.Println("Existing Race:", existingRace) - - raceValidator := validators.CreateRaceValidator{ - Duration: existingRace.Duration, - ElapsedTime: existingRace.ElapsedTime, - Laps: existingRace.Laps, - RaceType: existingRace.RaceType, - AverageSpeed: existingRace.AverageSpeed, - TotalFaults: existingRace.TotalFaults, - EffectiveDuration: existingRace.EffectiveDuration, - UserID: existingRace.UserID, - VehicleID: existingRace.VehicleID, + var raceValidator validators.UpdateRaceValidator + + // Valide les données JSON + if err := c.ShouldBindJSON(&raceValidator); err != nil { + services.SetJsonBindingErrorResponse(c, err) + return } if err := raceValidator.Validate(); err != nil { @@ -41,9 +40,32 @@ func UpdateRaceHandler(c *gin.Context) { return } - existingRace.Update(raceValidator) + // Mise à jour uniquement du champ End_time + if raceValidator.EndTime != nil && !raceValidator.EndTime.IsZero() { + if raceValidator.EndTime.Before(existingRace.StartTime.Time) { + services.SetUnprocessableEntity(c, "EndTime cannot be before StartTime") + + return + } + + existingRace.EndTime = raceValidator.EndTime + } + + if raceValidator.Name != "" { + existingRace.Name = raceValidator.Name + } + + if raceValidator.Status != "" { + existingRace.Status = raceValidator.Status + } + + if err := db.Save(&existingRace).Error; err != nil { + fmt.Printf("Error updating Race: %v\n", err) + services.SetInternalServerError(c, "Failed to update Race : "+err.Error()) + return + } - db.Save(&existingRace) + db.Preload("Vehicle").First(&existingRace, raceID) - services.SetCreated(c, "Race updated successfully", existingRace) + services.SetOK(c, "Race updated successfully", existingRace) } diff --git a/src/controllers/vehicle/getVehicleHandler.go b/src/controllers/vehicle/getVehicleHandler.go new file mode 100644 index 0000000..902c045 --- /dev/null +++ b/src/controllers/vehicle/getVehicleHandler.go @@ -0,0 +1,31 @@ +package vehicle + +import ( + "api/src/models" + "api/src/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm/clause" +) + +func GetVehicleHandler(c *gin.Context) { + vehicleId := c.Param("id") + + if vehicleId == "" || vehicleId == "0" || vehicleId == ":id" { + services.SetUnprocessableEntity(c, "Vehicle id is required") + + return + } + + connection := services.GetConnection() + + var vehicle models.Vehicle + + if connection.Where("id = ?", vehicleId).Preload(clause.Associations).First(&vehicle).RowsAffected == 0 { + services.SetUnprocessableEntity(c, "Vehicle not found") + + return + } + + services.SetOK(c, "Vehicle successfully retrieved", vehicle) +} diff --git a/src/controllers/vehicle/indexVehicleHandler.go b/src/controllers/vehicle/indexVehicleHandler.go new file mode 100644 index 0000000..5225561 --- /dev/null +++ b/src/controllers/vehicle/indexVehicleHandler.go @@ -0,0 +1,19 @@ +package vehicle + +import ( + "api/src/models" + "api/src/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm/clause" +) + +func IndexVehicleHandler(c *gin.Context) { + connection := services.GetConnection() + + var vehicles []models.Vehicle + + connection.Preload(clause.Associations).Find(&vehicles) + + services.SetOK(c, "Vehicles successfully retrieved", vehicles) +} diff --git a/src/controllers/vehicleController.go b/src/controllers/vehicleController.go deleted file mode 100644 index f5f3338..0000000 --- a/src/controllers/vehicleController.go +++ /dev/null @@ -1,124 +0,0 @@ -package controllers - -import ( - "api/src/models" - "api/src/services" - "fmt" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" -) - -// Helper function to transform vehicle model to API response format. -func transformVehicleToResponse(vehicle models.Vehicle) gin.H { - return gin.H{ - "data": gin.H{ - "type": "vehicule", - "id": strconv.Itoa(int(vehicle.ID)), - "attributes": vehicleToAttributes(vehicle), - }, - } -} - -// Helper function to map vehicle fields to response attributes. -func vehicleToAttributes(vehicle models.Vehicle) gin.H { - return gin.H{ - "battery_life": vehicle.BatteryLife, - "vehicle_name": vehicle.Name, - "line_sensors": []bool{vehicle.LineSensor1, vehicle.LineSensor2, vehicle.LineSensor3}, // Assume 3 line sensors - "camera": vehicle.Camera, - "sonar_range": vehicle.SonarRange, - "wheel_power": []int{vehicle.WheelPower1, vehicle.WheelPower2, vehicle.WheelPower3, vehicle.WheelPower4}, // 4 wheel powers - "led_color": vehicle.LedColor, - "display_panel": vehicle.DisplayPanel, - "speaker_status": vehicle.SpeakerStatus, - "sound_playing": vehicle.SoundPlaying, - } -} - -func GetVehicle(c *gin.Context) { - id := c.Param("id") - var vehicle models.Vehicle - result := services.GetConnection().First(&vehicle, id) - if result.Error != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Vehicle not found"}) - return - } - - c.JSON(http.StatusOK, transformVehicleToResponse(vehicle)) -} - -func GetVehicles(c *gin.Context) { - fmt.Println("GetVehicles") - var vehicles []models.Vehicle - result := services.GetConnection().Find(&vehicles) - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) - return - } - - var vehiclesResponse []gin.H - for _, vehicle := range vehicles { - vehiclesResponse = append(vehiclesResponse, transformVehicleToResponse(vehicle)) - } - - c.JSON(http.StatusOK, gin.H{"data": vehiclesResponse}) -} - -func CreateVehicle(c *gin.Context) { - var vehicle models.Vehicle - if err := c.BindJSON(&vehicle); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return - } - - result := services.GetConnection().Create(&vehicle) - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) - return - } - - c.JSON(http.StatusCreated, transformVehicleToResponse(vehicle)) -} - -func UpdateVehicle(c *gin.Context) { - id := c.Param("id") - var vehicle models.Vehicle - result := services.GetConnection().First(&vehicle, id) - if result.Error != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Vehicle not found"}) - return - } - - if err := c.BindJSON(&vehicle); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return - } - - result = services.GetConnection().Save(&vehicle) - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) - return - } - - c.JSON(http.StatusOK, transformVehicleToResponse(vehicle)) -} - -func DeleteVehicle(c *gin.Context) { - id := c.Param("id") - var vehicle models.Vehicle - result := services.GetConnection().First(&vehicle, id) - if result.Error != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Vehicle not found"}) - return - } - - result = services.GetConnection().Delete(&vehicle) - if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) - return - } - - c.JSON(http.StatusOK, gin.H{"meta": gin.H{"message": "Vehicule deleted successfully"}}) -} diff --git a/src/controllers/vehicle_state/getSingleVehiculeStateHandler.go b/src/controllers/vehicle_state/getSingleVehiculeStateHandler.go new file mode 100644 index 0000000..0861350 --- /dev/null +++ b/src/controllers/vehicle_state/getSingleVehiculeStateHandler.go @@ -0,0 +1,31 @@ +package vehicle_state + +import ( + "api/src/models" + "api/src/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm/clause" +) + +func GetASingleVehiculeStateHandler(c *gin.Context) { + vehiculeStateId := c.Param("id") + + if vehiculeStateId == "" || vehiculeStateId == "0" || vehiculeStateId == ":id" { + services.SetUnprocessableEntity(c, "Vehicle state id is required") + + return + } + + connection := services.GetConnection() + + var vehicleState models.VehicleState + + if connection.Where("id = ?", vehiculeStateId).Preload(clause.Associations).First(&vehicleState).RowsAffected == 0 { + services.SetUnprocessableEntity(c, "Vehicle state not found") + + return + } + + services.SetOK(c, "Vehicule state successfully retrieved", vehicleState) +} diff --git a/src/controllers/vehicle_state/updateVehicleStateHandler.go b/src/controllers/vehicle_state/updateVehicleStateHandler.go new file mode 100644 index 0000000..1580966 --- /dev/null +++ b/src/controllers/vehicle_state/updateVehicleStateHandler.go @@ -0,0 +1,105 @@ +package vehicle_state + +import ( + "api/src/models" + "api/src/services" + validators "api/src/validators/vehicleState" + + "github.com/gin-gonic/gin" + "gorm.io/gorm/clause" +) + +func UpdateVehicleStateHandler(c *gin.Context) { + vehiculeStateId := c.Param("id") + + if vehiculeStateId == "" || vehiculeStateId == "0" || vehiculeStateId == ":id" { + services.SetUnprocessableEntity(c, "Vehicle state not found") + return + } + + var UpdateVehicleStateValidator validators.UpdateVehicleStateValidator + + if err := c.ShouldBindJSON(&UpdateVehicleStateValidator); err != nil { + services.SetJsonBindingErrorResponse(c, err) + return + } + + if err := UpdateVehicleStateValidator.Validate(); err != nil { + services.SetValidationErrorResponse(c, err) + return + } + + connection := services.GetConnection() + + var vehicleState models.VehicleState + + if connection.Where("id = ?", vehiculeStateId).First(&vehicleState).RowsAffected == 0 { + services.SetUnprocessableEntity(c, "Vehicle state not found") + return + } + + connection.Model(models.VehicleState{}). + Where("id = ?", vehiculeStateId). + Updates(models.VehicleState{ + Face: UpdateVehicleStateValidator.Face, + LedAnimation: UpdateVehicleStateValidator.LedAnimation, + BuzzerAlarm: UpdateVehicleStateValidator.BuzzerAlarm, + VideoActivated: UpdateVehicleStateValidator.VideoActivated, + }) + + connection.Model(models.PrimaryLedColor{}). + Where("id = ?", vehicleState.PrimaryLedColorID). + Updates(models.PrimaryLedColor{ + LedIdentifier: UpdateVehicleStateValidator.PrimaryLedColor.LedIdentifier, + Red: UpdateVehicleStateValidator.PrimaryLedColor.Red, + Green: UpdateVehicleStateValidator.PrimaryLedColor.Green, + Blue: UpdateVehicleStateValidator.PrimaryLedColor.Blue, + }) + + connection.Model(models.SecondaryLedColor{}). + Where("id = ?", vehicleState.SecondaryLedColorID). + Updates(models.SecondaryLedColor{ + BinaryRepresentation: UpdateVehicleStateValidator.SecondaryLedColor.BinaryRepresentation, + Red: UpdateVehicleStateValidator.SecondaryLedColor.Red, + Green: UpdateVehicleStateValidator.SecondaryLedColor.Green, + Blue: UpdateVehicleStateValidator.SecondaryLedColor.Blue, + }) + + connection.Model(models.BuzzerVariable{}). + Where("id = ?", vehicleState.BuzzerVariableID). + Updates(models.BuzzerVariable{ + Activated: UpdateVehicleStateValidator.BuzzerVariable.Activated, + Frequency: UpdateVehicleStateValidator.BuzzerVariable.Frequency, + }) + + connection.Model(models.HeadAngle{}). + Where("id = ?", vehicleState.HeadAngleID). + Updates(models.HeadAngle{ + VerticalAngle: UpdateVehicleStateValidator.HeadAngle.VerticalAngle, + HorizontalAngle: UpdateVehicleStateValidator.HeadAngle.HorizontalAngle, + }) + + connection.Where("id = ?", vehiculeStateId).Preload(clause.Associations).First(&vehicleState) + + err := broadcastUpdatedVehicleState(vehicleState) + + if err != nil { + services.SetInternalServerError(c, "Error while broadcasting vehicle state update") + + return + } + + services.SetOK(c, "Vehicule state successfully updated", vehicleState) +} + +func broadcastUpdatedVehicleState(vehicleState models.VehicleState) error { + json, err := vehicleState.ToJson() + + if err != nil { + return err + } + + go services.BroadcastMessage(json) + + return nil +} diff --git a/src/middleware/mailMiddleware.go b/src/middleware/mailMiddleware.go new file mode 100644 index 0000000..ce31757 --- /dev/null +++ b/src/middleware/mailMiddleware.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "api/src/config" + "api/src/services" + "log" + + "github.com/gin-gonic/gin" +) + +func SendWelcomeEmailMiddleware(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + if c.Writer.Status() == 201 { + email, emailExists := c.Get("user_email") + username, usernameExists := c.Get("username") + + if !emailExists { + log.Println("user_email not found in context") + return + } + + if !usernameExists { + log.Println("username not found in context") + return + } + + log.Printf("Preparing to send welcome email to: %s", email) + + emailRequest := services.EmailRequest{ + To: email.(string), + Subject: "Welcome to TechRace!", + Template: "welcome_template.html", + Data: map[string]interface{}{ + "Username": username.(string), + }, + } + + err := services.SendEmail(emailRequest, cfg) + if err != nil { + log.Printf("Failed to send welcome email: %v", err) + c.JSON(500, gin.H{"error": "Failed to send welcome email"}) + } else { + log.Println("Welcome email sent successfully") + } + } else { + log.Printf("Response status is not 201: %d", c.Writer.Status()) + } + } +} diff --git a/src/models/BuzzerVariable.go b/src/models/BuzzerVariable.go new file mode 100644 index 0000000..def3b76 --- /dev/null +++ b/src/models/BuzzerVariable.go @@ -0,0 +1,9 @@ +package models + +type BuzzerVariable struct { + ID uint `gorm:"primaryKey"` + Activated *uint8 `gorm:"not null" json:"activated"` + Frequency *uint16 `gorm:"not null" json:"frequency"` + VehicleStateID uint `gorm:"not null"` + Model +} diff --git a/src/models/SecondaryLedColor.go b/src/models/SecondaryLedColor.go new file mode 100644 index 0000000..793a929 --- /dev/null +++ b/src/models/SecondaryLedColor.go @@ -0,0 +1,21 @@ +package models + +import "gorm.io/gorm" + +type SecondaryLedColor struct { + ID uint `gorm:"primaryKey"` + BinaryRepresentation *int `gorm:"not null" json:"binary_representation"` + Red *uint8 `gorm:"not null" json:"red"` + Green *uint8 `gorm:"not null" json:"green"` + Blue *uint8 `gorm:"not null" json:"blue"` + VehicleStateID uint `gorm:"not null"` + Model +} + +func CreateSecondaryLedColor(db *gorm.DB, secondaryLedColor *SecondaryLedColor) (*SecondaryLedColor, error) { + if err := db.Create(secondaryLedColor).Error; err != nil { + return nil, err + } + + return secondaryLedColor, nil +} diff --git a/src/models/VehicleBattery.go b/src/models/VehicleBattery.go new file mode 100644 index 0000000..0346c85 --- /dev/null +++ b/src/models/VehicleBattery.go @@ -0,0 +1,9 @@ +package models + +type VehicleBattery struct { + ID uint `gorm:"primaryKey"` + VehicleID uint `gorm:"not null" json:"vehicle_id"` + Vehicle Vehicle `gorm:"foreignKey:VehicleID" json:"vehicle"` + Value int `gorm:"not null" json:"value"` + Model +} diff --git a/src/models/attributes/customTime.go b/src/models/attributes/customTime.go new file mode 100644 index 0000000..9c97306 --- /dev/null +++ b/src/models/attributes/customTime.go @@ -0,0 +1,48 @@ +package attributes + +import ( + "database/sql/driver" + "fmt" + "time" +) + +type CustomTime struct { + time.Time +} + +const customTimeFormat = "2006-01-02 15:04:05" + +func (ct CustomTime) MarshalJSON() ([]byte, error) { + formatted := fmt.Sprintf("\"%s\"", ct.Format(customTimeFormat)) + return []byte(formatted), nil +} + +func (ct *CustomTime) UnmarshalJSON(b []byte) error { + str := string(b) + str = str[1 : len(str)-1] + t, err := time.Parse(customTimeFormat, str) + if err != nil { + return fmt.Errorf("invalid time format: %v", err) + } + ct.Time = t + return nil +} + +// Scan implémente l'interface sql.Scanner, permet de lire une valeur SQL et de la convertir en CustomTime +func (ct *CustomTime) Scan(value interface{}) error { + if value == nil { + *ct = CustomTime{Time: time.Time{}} + return nil + } + t, ok := value.(time.Time) + if !ok { + return fmt.Errorf("cannot scan type %T into CustomTime: %v", value, value) + } + *ct = CustomTime{Time: t} + return nil +} + +// Value implémente l'interface driver.Valuer, permet de convertir CustomTime en un type supporté par SQL +func (ct CustomTime) Value() (driver.Value, error) { + return ct.Time, nil +} diff --git a/src/models/foolModel.go b/src/models/foolModel.go deleted file mode 100644 index c7ced5b..0000000 --- a/src/models/foolModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package models - -import ( - validators "api/src/validators/fool" -) - -type Fool struct { - ID uint `gorm:"primaryKey"` - Name string `json:"name"` - Model -} - -func (f *Fool) Create(createFool validators.CreateFoolValidator) error { - if err := createFool.Validate(); err != nil { - return err - } - f.Name = createFool.Name - return nil -} - -func (f *Fool) Update(updateFool validators.CreateFoolValidator) error { - if err := updateFool.Validate(); err != nil { - return err - } - f.Name = updateFool.Name - return nil -} diff --git a/src/models/headAngle.go b/src/models/headAngle.go new file mode 100644 index 0000000..05d01f3 --- /dev/null +++ b/src/models/headAngle.go @@ -0,0 +1,9 @@ +package models + +type HeadAngle struct { + ID uint `gorm:"primaryKey"` + VerticalAngle *uint `gorm:"not null" json:"vertical_angle"` + HorizontalAngle *uint `gorm:"not null" json:"horizontal_angle"` + VehicleStateID uint `gorm:"not null"` + Model +} diff --git a/src/models/model.go b/src/models/model.go index 4ec7c76..241eb6b 100644 --- a/src/models/model.go +++ b/src/models/model.go @@ -1,13 +1,14 @@ package models import ( - "time" + "api/src/models/attributes" "gorm.io/gorm" ) type Model struct { - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` + // json format YYYY-MM-DD HH:MM:SS + CreatedAt attributes.CustomTime `gorm:"type:datetime" json:"created_at"` + UpdatedAt attributes.CustomTime `gorm:"type:datetime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"type:datetime;index" json:"-"` } diff --git a/src/models/primaryLedColor.go b/src/models/primaryLedColor.go new file mode 100644 index 0000000..455a270 --- /dev/null +++ b/src/models/primaryLedColor.go @@ -0,0 +1,11 @@ +package models + +type PrimaryLedColor struct { + ID uint `gorm:"primaryKey"` + LedIdentifier *int `gorm:"not null" json:"led_identifier"` + Red *uint8 `gorm:"not null" json:"red"` + Green *uint8 `gorm:"not null" json:"green"` + Blue *uint8 `gorm:"not null" json:"blue"` + VehicleStateID uint `gorm:"not null"` + Model +} diff --git a/src/models/race.go b/src/models/race.go new file mode 100644 index 0000000..9737999 --- /dev/null +++ b/src/models/race.go @@ -0,0 +1,23 @@ +package models + +import ( + "api/src/models/attributes" +) + +type Race struct { + ID uint `gorm:"primaryKey"` + Name string `json:"name"` + VehicleID uint `json:"vehicle_id"` + Vehicle Vehicle `gorm:"foreignKey:VehicleID" json:"vehicle"` + StartTime attributes.CustomTime `gorm:"type:datetime; not null" json:"start_time"` + EndTime *attributes.CustomTime `gorm:"type:datetime" json:"end_time"` + CollisionDuration int `gorm:"not null" json:"collision_duration"` + DistanceCovered int `gorm:"not null" json:"distance_covered"` + AverageSpeed float64 `gorm:"not null" json:"average_speed"` + OutOfParcours int `gorm:"not null" json:"out_of_parcours"` + Status string `gorm:"not null" json:"status"` + Type string `gorm:"not null" json:"type"` + UserID uint `json:"user_id"` + User User `gorm:"foreignKey:UserID" json:"-"` + Model +} diff --git a/src/models/raceModel.go b/src/models/raceModel.go deleted file mode 100644 index 558bf2c..0000000 --- a/src/models/raceModel.go +++ /dev/null @@ -1,53 +0,0 @@ -package models - -import ( - validators "api/src/validators/race" -) - -type Race struct { - ID uint `gorm:"primaryKey"` - Duration int - ElapsedTime int - Laps int - RaceType string - AverageSpeed int - TotalFaults int - EffectiveDuration int - UserID uint - VehicleID uint - Vehicle Vehicle `gorm:"foreignKey:VehicleID"` - Fool []Fool `gorm:"many2many:race_fool;" json:"fool"` - Model -} - -func (r *Race) Create(createRace validators.CreateRaceValidator) error { - if err := createRace.Validate(); err != nil { - return err - } - r.Duration = createRace.Duration - r.ElapsedTime = createRace.ElapsedTime - r.Laps = createRace.Laps - r.RaceType = createRace.RaceType - r.AverageSpeed = createRace.AverageSpeed - r.TotalFaults = createRace.TotalFaults - r.EffectiveDuration = createRace.EffectiveDuration - r.UserID = createRace.UserID - r.VehicleID = createRace.VehicleID - return nil -} - -func (r *Race) Update(updateRace validators.CreateRaceValidator) error { - if err := updateRace.Validate(); err != nil { - return err - } - r.Duration = updateRace.Duration - r.ElapsedTime = updateRace.ElapsedTime - r.Laps = updateRace.Laps - r.RaceType = updateRace.RaceType - r.AverageSpeed = updateRace.AverageSpeed - r.TotalFaults = updateRace.TotalFaults - r.EffectiveDuration = updateRace.EffectiveDuration - r.UserID = updateRace.UserID - r.VehicleID = updateRace.VehicleID - return nil -} diff --git a/src/models/sensorDataModel.go b/src/models/sensorDataModel.go deleted file mode 100644 index 9e04508..0000000 --- a/src/models/sensorDataModel.go +++ /dev/null @@ -1,33 +0,0 @@ -package models - -import ( - validators "api/src/validators/sensorData" -) - -type SensorData struct { - ID uint `gorm:"primaryKey"` - Light float64 - Sonar float64 - Track float64 - Model -} - -func (s *SensorData) Create(createSensorData validators.CreateSensorDataValidator) error { - if err := createSensorData.Validate(); err != nil { - return err - } - s.Light = createSensorData.Light - s.Sonar = createSensorData.Sonar - s.Track = createSensorData.Track - return nil -} - -func (s *SensorData) Update(updateSensorData validators.CreateSensorDataValidator) error { - if err := updateSensorData.Validate(); err != nil { - return err - } - s.Light = updateSensorData.Light - s.Sonar = updateSensorData.Sonar - s.Track = updateSensorData.Track - return nil -} diff --git a/src/models/userModel.go b/src/models/userModel.go index 45b6d06..42b390c 100644 --- a/src/models/userModel.go +++ b/src/models/userModel.go @@ -5,15 +5,16 @@ import ( ) type User struct { - ID uint `gorm:"primaryKey"` - Username string `gorm:"unique;not null" json:"username"` - Email string `gorm:"unique;not null" json:"email"` - Password string `gorm:"not null" json:"password"` + ID uint `gorm:"primaryKey" json:"id"` + Username *string `gorm:"unique" json:"username"` + Email string `gorm:"unique;not null" json:"email"` + Password string `gorm:"not null" json:"-"` Model } func (u *User) Update(updateUser validators.UpdateUserValidator) { - u.Username = updateUser.Username + u.Username = &updateUser.Username + u.Email = updateUser.Email if updateUser.Password != "" { @@ -22,7 +23,8 @@ func (u *User) Update(updateUser validators.UpdateUserValidator) { } func (u *User) Create(CreateUser validators.RegisterUserValidator) { - u.Username = CreateUser.Username + u.Username = &CreateUser.Username + u.Email = CreateUser.Email u.Password = CreateUser.Password } diff --git a/src/models/vehicle.go b/src/models/vehicle.go new file mode 100644 index 0000000..6cab04d --- /dev/null +++ b/src/models/vehicle.go @@ -0,0 +1,86 @@ +package models + +import "gorm.io/gorm" + +type Vehicle struct { + ID uint `gorm:"primaryKey"` + Name string `json:"name"` + IpAdress string `json:"ip_adress"` + IsAvailable bool `json:"is_available"` + Model +} + +func (vehicle *Vehicle) InitVehicleState(user *User, db *gorm.DB) (VehicleState, error) { + defaultUint := uint(0) + defaultUint8 := uint8(0) + defaultUint16 := uint16(0) + + vehicleState := VehicleState{ + VehicleID: vehicle.ID, + Face: &defaultUint8, + LedAnimation: &defaultUint8, + BuzzerAlarm: &defaultUint8, + VideoActivated: &defaultUint8, + UserID: user.ID, + } + + if err := db.Create(&vehicleState).Error; err != nil { + return VehicleState{}, err + } + + primaryLedColor := PrimaryLedColor{ + LedIdentifier: new(int), + Red: &defaultUint8, + Green: &defaultUint8, + Blue: &defaultUint8, + VehicleStateID: vehicleState.ID, + } + + if err := db.Create(&primaryLedColor).Error; err != nil { + return VehicleState{}, err + } + + vehicleState.PrimaryLedColor = &primaryLedColor + + secondaryLedColor := SecondaryLedColor{ + BinaryRepresentation: new(int), + Red: &defaultUint8, + Green: &defaultUint8, + Blue: &defaultUint8, + VehicleStateID: vehicleState.ID, + } + + if err := db.Create(&secondaryLedColor).Error; err != nil { + return VehicleState{}, err + } + + vehicleState.SecondaryLedColor = &secondaryLedColor + + buzzerVariable := BuzzerVariable{ + Activated: &defaultUint8, + Frequency: &defaultUint16, + VehicleStateID: vehicleState.ID, + } + + if err := db.Create(&buzzerVariable).Error; err != nil { + return VehicleState{}, err + } + + vehicleState.BuzzerVariable = &buzzerVariable + + headAngle := HeadAngle{ + VerticalAngle: &defaultUint, + HorizontalAngle: &defaultUint, + VehicleStateID: vehicleState.ID, + } + + if err := db.Create(&headAngle).Error; err != nil { + return VehicleState{}, err + } + + vehicleState.HeadAngle = &headAngle + + db.Save(&vehicleState) + + return vehicleState, nil +} diff --git a/src/models/vehicleModel.go b/src/models/vehicleModel.go deleted file mode 100644 index 2fbeedb..0000000 --- a/src/models/vehicleModel.go +++ /dev/null @@ -1,69 +0,0 @@ -package models - -import ( - validators "api/src/validators/vehicle" -) - -type Vehicle struct { - ID uint `gorm:"primaryKey"` - Name string `json:"vehicle_name"` - BatteryLife float64 `json:"battery_life"` - LineSensor1 bool `json:"line_sensor1"` - LineSensor2 bool `json:"line_sensor2"` - LineSensor3 bool `json:"line_sensor3"` - Camera bool `json:"camera"` - SonarRange float64 `json:"sonar_range"` - WheelPower1 int `json:"wheel_power1"` - WheelPower2 int `json:"wheel_power2"` - WheelPower3 int `json:"wheel_power3"` - WheelPower4 int `json:"wheel_power4"` - LedColor string `json:"led_color"` - DisplayPanel string `json:"display_panel"` - SpeakerStatus bool `json:"speaker_status"` - SoundPlaying string `json:"sound_playing"` - Model -} - -func (v *Vehicle) Create(createVehicle validators.CreateVehicleValidator) error { - if err := createVehicle.Validate(); err != nil { - return err - } - v.Name = createVehicle.Name - v.BatteryLife = createVehicle.BatteryLife - v.LineSensor1 = createVehicle.LineSensor1 - v.LineSensor2 = createVehicle.LineSensor2 - v.LineSensor3 = createVehicle.LineSensor3 - v.Camera = createVehicle.Camera - v.SonarRange = createVehicle.SonarRange - v.WheelPower1 = createVehicle.WheelPower1 - v.WheelPower2 = createVehicle.WheelPower2 - v.WheelPower3 = createVehicle.WheelPower3 - v.WheelPower4 = createVehicle.WheelPower4 - v.LedColor = createVehicle.LedColor - v.DisplayPanel = createVehicle.DisplayPanel - v.SpeakerStatus = createVehicle.SpeakerStatus - v.SoundPlaying = createVehicle.SoundPlaying - return nil -} - -func (v *Vehicle) Update(updateVehicle validators.CreateVehicleValidator) error { - if err := updateVehicle.Validate(); err != nil { - return err - } - v.Name = updateVehicle.Name - v.BatteryLife = updateVehicle.BatteryLife - v.LineSensor1 = updateVehicle.LineSensor1 - v.LineSensor2 = updateVehicle.LineSensor2 - v.LineSensor3 = updateVehicle.LineSensor3 - v.Camera = updateVehicle.Camera - v.SonarRange = updateVehicle.SonarRange - v.WheelPower1 = updateVehicle.WheelPower1 - v.WheelPower2 = updateVehicle.WheelPower2 - v.WheelPower3 = updateVehicle.WheelPower3 - v.WheelPower4 = updateVehicle.WheelPower4 - v.LedColor = updateVehicle.LedColor - v.DisplayPanel = updateVehicle.DisplayPanel - v.SpeakerStatus = updateVehicle.SpeakerStatus - v.SoundPlaying = updateVehicle.SoundPlaying - return nil -} diff --git a/src/models/vehicleState.go b/src/models/vehicleState.go new file mode 100644 index 0000000..b98f3a0 --- /dev/null +++ b/src/models/vehicleState.go @@ -0,0 +1,67 @@ +package models + +import "encoding/json" + +type VehicleState struct { + ID uint `gorm:"primaryKey" json:"id"` + VehicleID uint `json:"-"` + //Vehicle Vehicle `gorm:"foreignKey:VehicleID"` + Face *uint8 `gorm:"not null" json:"face"` + LedAnimation *uint8 `gorm:"not null" json:"led_animation"` + BuzzerAlarm *uint8 `gorm:"not null" json:"buzzer_alarm"` + VideoActivated *uint8 `gorm:"not null" json:"video_activated"` + PrimaryLedColorID *uint `json:"-"` + PrimaryLedColor *PrimaryLedColor `gorm:"foreignKey:PrimaryLedColorID"` + SecondaryLedColorID *uint `json:"-"` + SecondaryLedColor *SecondaryLedColor `gorm:"foreignKey:SecondaryLedColorID"` + BuzzerVariableID *uint `json:"-"` + BuzzerVariable *BuzzerVariable `gorm:"foreignKey:BuzzerVariableID"` + HeadAngleID *uint `json:"-"` + HeadAngle *HeadAngle `gorm:"foreignKey:HeadAngleID"` + UserID uint `json:"-"` + Model +} + +func (vehicleState *VehicleState) ToJson() (string, error) { + type vehicleStateJson struct { + Type string `json:"type"` + ID uint `json:"id"` + Attributes interface{} `json:"attributes"` + } + + type attributes struct { + Face *uint8 `json:"face"` + LedAnimation *uint8 `json:"led_animation"` + BuzzerAlarm *uint8 `json:"buzzer_alarm"` + VideoActivated *uint8 `json:"video_activated"` + PrimaryLedColor interface{} `json:"primary_led_color,omitempty"` + SecondaryLedColor interface{} `json:"secondary_led_color,omitempty"` + BuzzerVariable interface{} `json:"buzzer_variable,omitempty"` + HeadAngle interface{} `json:"head_angle,omitempty"` + } + + attr := attributes{ + Face: vehicleState.Face, + LedAnimation: vehicleState.LedAnimation, + BuzzerAlarm: vehicleState.BuzzerAlarm, + VideoActivated: vehicleState.VideoActivated, + PrimaryLedColor: vehicleState.PrimaryLedColor, + SecondaryLedColor: vehicleState.SecondaryLedColor, + BuzzerVariable: vehicleState.BuzzerVariable, + HeadAngle: vehicleState.HeadAngle, + } + + vsJson := vehicleStateJson{ + Type: "vehicle_state", + ID: vehicleState.ID, + Attributes: attr, + } + + jsonData, err := json.Marshal(vsJson) + + if err != nil { + return "", err + } + + return string(jsonData), nil +} diff --git a/src/routes/protected/setupProtectedRoutes.go b/src/routes/protected/setupProtectedRoutes.go index 7d4c6ab..915ff10 100644 --- a/src/routes/protected/setupProtectedRoutes.go +++ b/src/routes/protected/setupProtectedRoutes.go @@ -3,8 +3,9 @@ package protected import ( controllers "api/src/controllers" race_controller "api/src/controllers/race" + "api/src/controllers/vehicle" + "api/src/controllers/vehicle_state" - //user_controller "api/src/controllers/user" "api/src/middleware" "github.com/gin-gonic/gin" @@ -15,34 +16,34 @@ func SetupProtectedRoutes(routerGroup *gin.RouterGroup) { routerGroup.GET("/protected", controllers.Welcome) - vehicleGroup := routerGroup.Group("/vehicles") - { - vehicleGroup.GET("/:id", controllers.GetVehicle) - vehicleGroup.GET("/", controllers.GetVehicles) - vehicleGroup.POST("/", controllers.CreateVehicle) - vehicleGroup.PATCH("/:id", controllers.UpdateVehicle) - vehicleGroup.DELETE("/:id", controllers.DeleteVehicle) - - } - - /*userGroup := routerGroup.Group("/user") + userGroup := routerGroup.Group("/users") // USER ROUTES { - userGroup.GET("/:userId", user_controller.UserHandler) - userGroup.PUT("/:userId", user_controller.UpdateUserHandler) - userGroup.GET("/", user_controller.GetAllUserHandler) - userGroup.DELETE("/:userId", user_controller.DeleteUserHandler) - }*/ + userGroup.POST("/:userId/races", race_controller.CreateRaceHandler) + userGroup.GET("/:userId/races", race_controller.GetAllUsersRaceHandler) - raceGroup := routerGroup.Group("/race") + } // RACE ROUTES + raceGroup := routerGroup.Group("/races") { raceGroup.GET("/", race_controller.GetAllRaceHandler) - raceGroup.GET("/:raceId", race_controller.GetSingleRaceHandler) - raceGroup.POST("/", race_controller.CreateRaceHandler) raceGroup.PATCH("/:raceId", race_controller.UpdateRaceHandler) raceGroup.DELETE("/:raceId", race_controller.DeleteRaceHandler) } + // VEHICLE ROUTES + vehicleGroup := routerGroup.Group("/vehicles") + { + vehicleGroup.GET("/", vehicle.IndexVehicleHandler) + vehicleGroup.GET("/:id", vehicle.GetVehicleHandler) + } + + // VEHICLE STATE ROUTES + vehicleStateGroup := routerGroup.Group("/vehicle-states") + { + vehicleStateGroup.PATCH("/:id", vehicle_state.UpdateVehicleStateHandler) + vehicleStateGroup.GET("/:id", vehicle_state.GetASingleVehiculeStateHandler) + } + } diff --git a/src/routes/public/setupPublicRoutes.go b/src/routes/public/setupPublicRoutes.go index a7f75d2..13b8e53 100644 --- a/src/routes/public/setupPublicRoutes.go +++ b/src/routes/public/setupPublicRoutes.go @@ -1,14 +1,16 @@ package public import ( + "api/src/config" "api/src/controllers" + "api/src/middleware" + "api/src/services" "github.com/gin-gonic/gin" ) -func SetupPublicRoutes(routerGroup *gin.RouterGroup) { - routerGroup.POST("/signup", controllers.Signup) - +func SetupPublicRoutes(routerGroup *gin.RouterGroup, cfg *config.Config) { + routerGroup.POST("/signup", controllers.Signup, middleware.SendWelcomeEmailMiddleware(cfg)) routerGroup.POST("/login", controllers.Login) routerGroup.GET("/hello", func(c *gin.Context) { @@ -16,4 +18,6 @@ func SetupPublicRoutes(routerGroup *gin.RouterGroup) { "message": "Hello World!", }) }) + + routerGroup.GET("/sse", services.SSEHandler) } diff --git a/src/routes/routes.go b/src/routes/routes.go index de481d0..dbecda8 100644 --- a/src/routes/routes.go +++ b/src/routes/routes.go @@ -1,6 +1,7 @@ package routes import ( + "api/src/config" "api/src/routes/protected" "api/src/routes/public" "os" @@ -12,12 +13,13 @@ import ( func SetupCors(router *gin.Engine) *gin.Engine { allowedOrigins := []string{ os.Getenv("APP_FRONTEND_URL"), + "http://127.0.0.1:5173", } corsConfig := cors.Config{ AllowOrigins: allowedOrigins, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, } @@ -27,12 +29,12 @@ func SetupCors(router *gin.Engine) *gin.Engine { return router } -func SetupRouter(router *gin.Engine) *gin.Engine { +func SetupRouter(router *gin.Engine, cfg *config.Config) *gin.Engine { router = SetupCors(router) apiGroup := router.Group("/api") - public.SetupPublicRoutes(apiGroup) + public.SetupPublicRoutes(apiGroup, cfg) protected.SetupProtectedRoutes(apiGroup) diff --git a/src/services/email_validator.go b/src/services/email_validator.go new file mode 100644 index 0000000..7944fa8 --- /dev/null +++ b/src/services/email_validator.go @@ -0,0 +1,9 @@ +package services + +import "regexp" + +func EmailValidator(email string) bool { + re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + + return re.MatchString(email) +} diff --git a/src/services/mqttService.go b/src/services/mqttService.go index 78d51ab..8eaf9b4 100644 --- a/src/services/mqttService.go +++ b/src/services/mqttService.go @@ -1,42 +1,46 @@ package services import ( - "api/src/models" "fmt" - "strconv" + "strings" mqtt "github.com/eclipse/paho.mqtt.golang" ) func messagePubHandler(client mqtt.Client, msg mqtt.Message) { - fmt.Println("Received message:") + topic := msg.Topic() payload := string(msg.Payload()) - fmt.Println(payload) - fmt.Println(msg.Topic()) - value, err := strconv.ParseFloat(payload, 64) - if err != nil { - fmt.Println("Error converting payload to float:", err) + messageParts := strings.Split(topic, "/") + + if messageParts[0] != "esp32" { return } - var sensorData models.SensorData - - switch msg.Topic() { - case "esp32/track": - sensorData.Track = value - case "esp32/sonar": - sensorData.Sonar = value - case "esp32/light": - sensorData.Light = value - default: - fmt.Println("Invalid topic") + if len(messageParts) < 4 { + fmt.Println("Invalid message received. Must have at least 4 parts.") return } - result := GetConnection().Create(&sensorData) - if result.Error != nil { - fmt.Println("Error inserting data into database:", result.Error) + mqttHandler := MQTTHandler{} + + model := messageParts[1] + id := messageParts[2] + column := messageParts[3] + + // fmt.Println("############### MESSAGE RECEIVED ###############") + // fmt.Println(msg.Topic()) + // fmt.Println(payload) + // fmt.Println("models: ", model) + // fmt.Println("id: ", id) + // fmt.Println("column: ", column) + // fmt.Println("############# END #################") + + switch model { + case "races": + mqttHandler.HandleMQTTRaceData(id, column, payload) + default: + return } } diff --git a/src/services/mqtt_handler.go b/src/services/mqtt_handler.go new file mode 100644 index 0000000..ebd6347 --- /dev/null +++ b/src/services/mqtt_handler.go @@ -0,0 +1,82 @@ +package services + +import ( + "api/src/models" + "api/src/models/attributes" + "fmt" + "strconv" + "strings" + "time" +) + +type MQTTHandler struct{} + +func (h MQTTHandler) HandleMQTTRaceData(id string, column string, payload string) { + connection := GetConnection() + + var race models.Race + + if connection.Where("id = ?", id).First(&race).RowsAffected == 0 { + return + } + + var columnToUpdate string = column + var valueToUpdate interface{} + payload = strings.TrimSpace(payload) + + switch column { + case "distance_covered", "out_of_parcours", "collision_duration": + // we are always expecting a float from the ESP32 because of the way we are sending the data + payloadToFloat, err := strconv.ParseFloat(payload, 64) + + if err != nil { + fmt.Printf("Error while converting payload to float for %s: %v\n", column, err) + return + } + + valueToUpdate = int(payloadToFloat) + + case "average_speed": + // we are always expecting a float from the ESP32 because of the way we are sending the data + payloadToFloat, err := strconv.ParseFloat(payload, 64) + + if err != nil { + fmt.Printf("Error while converting payload to float for %s: %v\n", column, err) + return + } + + valueToUpdate = payloadToFloat + + case "status": + valueToUpdate = payload + + default: + return + } + + if race.Status == "completed" { + fmt.Printf("['mqtt_handler] an update was requested for a completed race: %d - ignoring\n", race.ID) + return + } + + if race.Status == "not_started" { + connection.Model(&race).Update("status", "in_progress") + } + + if columnToUpdate == "status" { + connection.Model(&race).Update("status", valueToUpdate) + + if valueToUpdate == "completed" { + + race.EndTime = &attributes.CustomTime{ + Time: time.Now(), + } + + connection.Model(&race).Update("end_time", race.EndTime) + } + + return + } + + connection.Model(&race).Update(columnToUpdate, valueToUpdate) +} diff --git a/src/services/sendEmailService.go b/src/services/sendEmailService.go new file mode 100644 index 0000000..f9b75bd --- /dev/null +++ b/src/services/sendEmailService.go @@ -0,0 +1,63 @@ +// services/send_email.go +package services + +import ( + "bytes" + "html/template" + "log" + "os" + "path/filepath" + + "api/src/config" + + "gopkg.in/mail.v2" +) + +type EmailRequest struct { + To string + Subject string + Template string + Data map[string]interface{} +} + +func SendEmail(request EmailRequest, cfg *config.Config) error { + templatePath := os.Getenv("TEMPLATE_PATH") + fullPath := filepath.Join(templatePath, request.Template) + + log.Printf("Loading email template from: %s", fullPath) + + t, err := template.ParseFiles(fullPath) + if err != nil { + log.Printf("Error loading template: %v", err) + return err + } + + var buf bytes.Buffer + err = t.Execute(&buf, request.Data) + if err != nil { + log.Printf("Error executing template: %v", err) + return err + } + + log.Printf("Sending email to: %s with subject: %s", request.To, request.Subject) + + m := mail.NewMessage() + m.SetHeader("From", cfg.EmailFrom) + m.SetHeader("To", request.To) + m.SetHeader("Subject", request.Subject) + m.SetBody("text/html", buf.String()) + + d := mail.NewDialer(cfg.SmtpHost, 587, cfg.SmtpUser, cfg.SmtpPass) + + // Log SMTP configuration avant l'envoi + log.Printf("SMTP Host: %s, SMTP User: %s", cfg.SmtpHost, cfg.SmtpUser) + + if err := d.DialAndSend(m); err != nil { + log.Printf("Error sending email: %v", err) + return err + } + + log.Println("Email sent successfully") + + return nil +} diff --git a/src/services/server_side_event.go b/src/services/server_side_event.go new file mode 100644 index 0000000..d01d01e --- /dev/null +++ b/src/services/server_side_event.go @@ -0,0 +1,50 @@ +package services + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +var clients []chan string + +func SSEHandler(c *gin.Context) { + flusher, ok := c.Writer.(http.Flusher) + if !ok { + http.Error(c.Writer, "Streaming unsupported!", http.StatusInternalServerError) + return + } + + messageChan := make(chan string) + clients = append(clients, messageChan) + + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + + defer func() { + for i, client := range clients { + if client == messageChan { + clients = append(clients[:i], clients[i+1:]...) + break + } + } + }() + + for { + select { + case msg := <-messageChan: + fmt.Fprintf(c.Writer, "data: %s\n\n", msg) + flusher.Flush() + case <-c.Request.Context().Done(): + return + } + } +} + +func BroadcastMessage(message string) { + for _, client := range clients { + client <- message + } +} diff --git a/src/templates/welcome_template.html b/src/templates/welcome_template.html new file mode 100644 index 0000000..a17f3be --- /dev/null +++ b/src/templates/welcome_template.html @@ -0,0 +1,15 @@ + + + + + Bienvenue sur TechRace ! + + +

Bienvenue sur TechRace, {{.Username}}!

+

Nous sommes ravis de vous accueillir au sein de notre communauté TechRace, où vous pouvez explorer le monde passionnant des voitures télécommandées.

+

Préparez-vous à découvrir des courses palpitantes, des véhicules innovants, et une communauté de passionnés tout aussi enthousiastes que vous.

+

Si vous avez des questions ou avez besoin d'assistance, notre équipe est là pour vous aider. N'hésitez pas à nous contacter à tout moment !

+

Merci de nous avoir rejoints et amusez-vous bien,

+

L'équipe TechRace

+ + diff --git a/src/tests/auth/login/can_login_if_valid_email_and_password_are_provided_test.go b/src/tests/auth/login/can_login_if_valid_email_and_password_are_provided_test.go index f997d73..2cd9500 100644 --- a/src/tests/auth/login/can_login_if_valid_email_and_password_are_provided_test.go +++ b/src/tests/auth/login/can_login_if_valid_email_and_password_are_provided_test.go @@ -21,10 +21,12 @@ func Test_can_login_if_valid_email_and_password_are_provided(t *testing.T) { hashedPassword, _ := services.HashPassword("password") + username := "username" + user := models.User{ Email: "test@example.com", Password: string(hashedPassword), - Username: "username", + Username: &username, } databaseConnection.Create(&user) diff --git a/src/tests/auth/login/cannot_login_if_invalid_password_is_provided_test.go b/src/tests/auth/login/cannot_login_if_invalid_password_is_provided_test.go index 25c9ce2..43a9f31 100644 --- a/src/tests/auth/login/cannot_login_if_invalid_password_is_provided_test.go +++ b/src/tests/auth/login/cannot_login_if_invalid_password_is_provided_test.go @@ -21,10 +21,12 @@ func Test_cannot_login_if_invalid_password_is_provided(t *testing.T) { hashedPassword, _ := services.HashPassword("password") + username := "username" + user := models.User{ Email: "test@example.com", Password: string(hashedPassword), - Username: "username", + Username: &username, } databaseConnection.Create(&user) diff --git a/src/tests/auth/signup/can_signup_if_email_and_password_are_provided_test.go b/src/tests/auth/signup/can_signup_if_email_and_password_are_provided_test.go index adec20d..bf74e12 100644 --- a/src/tests/auth/signup/can_signup_if_email_and_password_are_provided_test.go +++ b/src/tests/auth/signup/can_signup_if_email_and_password_are_provided_test.go @@ -4,6 +4,7 @@ import ( "api/src/models" "api/src/tests" "encoding/json" + "log" "net/http" "testing" @@ -14,33 +15,58 @@ import ( func Test_can_signup_if_email_and_password_are_provided_test(t *testing.T) { gin.SetMode(gin.TestMode) + // Initialisez la connexion à la base de données de test databaseConnection := tests.GetTestDBConnection() - // Créer une requête de test et la table associée - databaseConnection.AutoMigrate(&models.User{}) + // Effectuez les migrations pour toutes les tables nécessaires + err := databaseConnection.AutoMigrate( + &models.User{}, + &models.Vehicle{}, + &models.VehicleState{}, + &models.PrimaryLedColor{}, + &models.SecondaryLedColor{}, + &models.BuzzerVariable{}, + &models.HeadAngle{}, + &models.VehicleBattery{}, + ) + if err != nil { + log.Fatalf("Failed to migrate database: %v", err) + } + + // Préparez les données nécessaires pour le test + databaseConnection.Create(&models.Vehicle{ + Name: "A vehicle is required to enable any user to signup", + }) + // Données utilisateur pour le test user := map[string]string{ "email": "test@example.com", "password": "password", } body, _ := json.Marshal(user) - // On effecture une requête unauthenticated + // Effectuez une requête POST non authentifiée pour le signup requestRecorder, _ := tests.PerformUnAuthenticatedRequest(http.MethodPost, "/api/signup", body) - // Vérifier le statut de la réponse - assert.Equal(t, http.StatusCreated, requestRecorder.Code) + // Vérifiez le statut de la réponse + assert.Equal(t, http.StatusCreated, requestRecorder.Code, "Expected status code 201, got %d", requestRecorder.Code) - // Vérifier le contenu de la réponse - var response map[string]string - json.Unmarshal(requestRecorder.Body.Bytes(), &response) - assert.Equal(t, "User created", response["message"]) + // Vérifiez le contenu de la réponse + var response map[string]interface{} + err = json.Unmarshal(requestRecorder.Body.Bytes(), &response) + if err != nil { + t.Fatalf("Failed to unmarshal response body: %v", err) + } + assert.Equal(t, "User created", response["message"], "Expected message 'User created', got '%s'", response["message"]) - // Vérifier que l'utilisateur a été créé dans la base de données + // Vérifiez que l'utilisateur a été créé dans la base de données var createdUser models.User - databaseConnection.Where("email = ?", user["email"]).First(&createdUser) - assert.Equal(t, user["email"], createdUser.Email) + result := databaseConnection.Where("email = ?", user["email"]).First(&createdUser) + if result.Error != nil { + t.Fatalf("Expected user with email %s to be created, but got error: %v", user["email"], result.Error) + } + assert.Equal(t, user["email"], createdUser.Email, "Expected email to be '%s', got '%s'", user["email"], createdUser.Email) - // On retire l'utilisateur de la base de données + // Nettoyez la base de données en supprimant l'utilisateur créé databaseConnection.Unscoped().Delete(&createdUser) } diff --git a/src/tests/auth/signup/cannot_signup_if_email_is_not_provided_test.go b/src/tests/auth/signup/cannot_signup_if_email_is_not_provided_test.go index 1e6d1d1..70b7bd3 100644 --- a/src/tests/auth/signup/cannot_signup_if_email_is_not_provided_test.go +++ b/src/tests/auth/signup/cannot_signup_if_email_is_not_provided_test.go @@ -21,6 +21,7 @@ func Test_cannot_signup_if_email_is_not_provided_test(t *testing.T) { user := map[string]string{ "password": "password", } + body, _ := json.Marshal(user) requestRecorder, _ := tests.PerformUnAuthenticatedRequest(http.MethodPost, "/api/signup", body) diff --git a/src/tests/auth/signup/cannot_signup_if_password_is_not_provided_test.go b/src/tests/auth/signup/cannot_signup_if_password_is_not_provided_test.go index f8b9154..4413ad5 100644 --- a/src/tests/auth/signup/cannot_signup_if_password_is_not_provided_test.go +++ b/src/tests/auth/signup/cannot_signup_if_password_is_not_provided_test.go @@ -19,8 +19,9 @@ func Test_cannot_signup_if_password_is_not_provided_test(t *testing.T) { databaseConnection.AutoMigrate(&models.User{}) user := map[string]string{ - "email": "test@test.com", + "email": "test123@test.com", } + body, _ := json.Marshal(user) requestRecorder, _ := tests.PerformUnAuthenticatedRequest(http.MethodPost, "/api/signup", body) diff --git a/src/tests/database/testing_database_is_working_as_expected.go b/src/tests/database/testing_database_is_working_as_expected_test.go similarity index 100% rename from src/tests/database/testing_database_is_working_as_expected.go rename to src/tests/database/testing_database_is_working_as_expected_test.go diff --git a/src/tests/race/create_race_test.go b/src/tests/race/create_race_test.go new file mode 100644 index 0000000..4ec3e26 --- /dev/null +++ b/src/tests/race/create_race_test.go @@ -0,0 +1,60 @@ +package race + +import ( + "api/src/models" + "api/src/models/attributes" + "api/src/tests" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func Test_create_race_successfully(t *testing.T) { + gin.SetMode(gin.TestMode) + + databaseConnection := tests.GetTestDBConnection() + + // Ensure the necessary migrations are run + databaseConnection.AutoMigrate(&models.User{}, &models.Vehicle{}, &models.Race{}) + // Create a mock user + user := models.User{ + Email: "testuser@example.com", + Password: "securepassword", + } + databaseConnection.Create(&user) + // Create a mock vehicle + vehicle := models.Vehicle{ + Name: "Toyota", + } + databaseConnection.Create(&vehicle) + + // Define start and end times for the race + startTime := attributes.CustomTime{Time: time.Now()} + endTime := &attributes.CustomTime{Time: startTime.Add(time.Minute)} + + // Prepare the JSON body for the POST request + createBody, _ := json.Marshal(map[string]interface{}{ + "name": "testuser", + "start_time": startTime, + "end_time": endTime, + "collision_duration": 5, + "distance_covered": 150, + "average_speed": 130, + "out_of_parcours": 1, + "user_id": user.ID, + "vehicle_id": vehicle.ID, + "type": "manual", + "status": "not_started", + }) + + // Perform the authenticated request + requestURL := fmt.Sprintf("/api/users/%d/races", user.ID) // Use /users/:userId/races + requestRecorder, _ := tests.PerformAuthenticatedRequest(http.MethodPost, requestURL, createBody) + + assert.Equal(t, http.StatusCreated, requestRecorder.Code) +} diff --git a/src/tests/race/delete_race_test.go b/src/tests/race/delete_race_test.go new file mode 100644 index 0000000..53e509d --- /dev/null +++ b/src/tests/race/delete_race_test.go @@ -0,0 +1,66 @@ +package race + +import ( + "api/src/models" + "api/src/models/attributes" + "api/src/tests" + "fmt" + "net/http" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func Test_delete_race_successfully(t *testing.T) { + gin.SetMode(gin.TestMode) + + databaseConnection := tests.GetTestDBConnection() + + databaseConnection.AutoMigrate(&models.Vehicle{}, &models.Race{}) + + vehicle := models.Vehicle{ + Name: "Toyota", + } + databaseConnection.Create(&vehicle) + + var startTime attributes.CustomTime + startTime.Time = time.Now() + + race := models.Race{ + VehicleID: vehicle.ID, + StartTime: startTime, + CollisionDuration: 3, + DistanceCovered: 100, + AverageSpeed: 10, + OutOfParcours: 0, + UserID: 1, + Type: "manual", + } + databaseConnection.Create(&race) + + requestURL := fmt.Sprintf("/api/races/%d", race.ID) + requestRecorder, _ := tests.PerformAuthenticatedRequest(http.MethodDelete, requestURL, nil) + + assert.Equal(t, http.StatusNoContent, requestRecorder.Code) +} + +func Test_delete_race_not_found(t *testing.T) { + gin.SetMode(gin.TestMode) + + databaseConnection := tests.GetTestDBConnection() + + databaseConnection.AutoMigrate(&models.Vehicle{}, &models.Race{}) + + vehicle := models.Vehicle{ + Name: "Toyota", + } + databaseConnection.Create(&vehicle) + + nonExistentRaceID := 999 + requestURL := fmt.Sprintf("/api/races/%d", nonExistentRaceID) + requestRecorder, _ := tests.PerformAuthenticatedRequest(http.MethodDelete, requestURL, nil) + + assert.Equal(t, http.StatusNotFound, requestRecorder.Code) +} diff --git a/src/tests/race/get_index_race_test.go b/src/tests/race/get_index_race_test.go new file mode 100644 index 0000000..5bae393 --- /dev/null +++ b/src/tests/race/get_index_race_test.go @@ -0,0 +1,79 @@ +package race + +import ( + "api/src/models" + "api/src/models/attributes" + "api/src/tests" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +type ApiResponse struct { + Message string `json:"message"` + Data []models.Race `json:"data"` +} + +func Test_get_races_index(t *testing.T) { + gin.SetMode(gin.TestMode) + + databaseConnection := tests.GetTestDBConnection() + + // Ensure the necessary migrations are run + databaseConnection.AutoMigrate(&models.User{}, &models.Vehicle{}, &models.Race{}) + + // Create a mock user + user := models.User{ + Email: "testuser2@example.com", + Password: "securepassword", + } + databaseConnection.Create(&user) + + // Create a mock vehicle + vehicle := models.Vehicle{ + Name: "Toyota", + } + databaseConnection.Create(&vehicle) + + // Create a mock race associated with the user + var startTime attributes.CustomTime + startTime.Time = time.Now() + + race := models.Race{ + VehicleID: vehicle.ID, + StartTime: startTime, + EndTime: nil, + CollisionDuration: 3, + DistanceCovered: 100, + AverageSpeed: 10, + OutOfParcours: 0, + UserID: user.ID, + Type: "manual", + Status: "Not Started", + } + databaseConnection.Create(&race) + + // Construct the request URL + requestURL := fmt.Sprintf("/api/users/%d/races", user.ID) + + // Perform the authenticated request + requestRecorder, _ := tests.PerformAuthenticatedRequest(http.MethodGet, requestURL, nil) + + // Assert the status code + assert.Equal(t, http.StatusOK, requestRecorder.Code) + + // Parse and check the response body + var response ApiResponse + err := json.Unmarshal(requestRecorder.Body.Bytes(), &response) + assert.NoError(t, err) + + // Validate the response data + races := response.Data + assert.NotNil(t, races) + assert.True(t, len(races) > 0) +} diff --git a/src/tests/race/update_race_test.go b/src/tests/race/update_race_test.go new file mode 100644 index 0000000..e437c62 --- /dev/null +++ b/src/tests/race/update_race_test.go @@ -0,0 +1,60 @@ +package race + +import ( + "api/src/models" + "api/src/models/attributes" + "api/src/tests" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func Test_update_race_successfully(t *testing.T) { + gin.SetMode(gin.TestMode) + + databaseConnection := tests.GetTestDBConnection() + + databaseConnection.AutoMigrate(&models.Vehicle{}, &models.Race{}) + + vehicle := models.Vehicle{ + Name: "Toyota", + } + databaseConnection.Create(&vehicle) + + var startTime attributes.CustomTime + startTime.Time = time.Now() + + race := models.Race{ + VehicleID: vehicle.ID, + StartTime: startTime, + EndTime: nil, + CollisionDuration: 3, + DistanceCovered: 100, + AverageSpeed: 10, + OutOfParcours: 0, + UserID: 1, + } + databaseConnection.Create(&race) + + endTime := &attributes.CustomTime{ + Time: startTime.Add(time.Minute), + } + + updateBody, _ := json.Marshal(map[string]interface{}{ + "end_time": endTime, + "status": "completed", + }) + + requestURL := fmt.Sprintf("/api/races/%d", race.ID) + requestRecorder, _ := tests.PerformAuthenticatedRequest(http.MethodPatch, requestURL, updateBody) + + assert.Equal(t, http.StatusOK, requestRecorder.Code) + + databaseConnection.Unscoped().Delete(&vehicle) + databaseConnection.Unscoped().Delete(&race) +} diff --git a/src/tests/setup_test_authenticated_user.go b/src/tests/setup_test_authenticated_user.go index f852e60..1f9673e 100644 --- a/src/tests/setup_test_authenticated_user.go +++ b/src/tests/setup_test_authenticated_user.go @@ -32,7 +32,7 @@ func CreateTestUser(db *gorm.DB) *models.User { hashedPassword, _ := services.HashPassword(password) testUser = &models.User{ - Username: username, + Username: &username, Email: email, Password: string(hashedPassword), } diff --git a/src/tests/setup_test_helpers.go b/src/tests/setup_test_helpers.go new file mode 100644 index 0000000..dfe3f9e --- /dev/null +++ b/src/tests/setup_test_helpers.go @@ -0,0 +1,23 @@ +package tests + +import ( + "api/src/models" + + "gorm.io/gorm" +) + +func SetupTestVehicle(db *gorm.DB) *models.Vehicle { + vehicle := &models.Vehicle{ + Name: "Test Vehicle", + IpAdress: "1.1.1.1", + IsAvailable: true, + } + + err := db.Create(&vehicle).Error + + if err != nil { + panic(err) + } + + return vehicle +} diff --git a/src/tests/setup_test_router.go b/src/tests/setup_test_router.go index bd86d20..5c054a1 100644 --- a/src/tests/setup_test_router.go +++ b/src/tests/setup_test_router.go @@ -1,8 +1,10 @@ package tests import ( + "api/src/config" "api/src/routes/protected" "api/src/routes/public" + "log" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -23,7 +25,12 @@ func setupTestRouter(router *gin.Engine) *gin.Engine { apiGroup := router.Group("/api") - public.SetupPublicRoutes(apiGroup) + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + public.SetupPublicRoutes(apiGroup, cfg) protected.SetupProtectedRoutes(apiGroup) diff --git a/src/validators/fool/createFool.go b/src/validators/fool/createFool.go deleted file mode 100644 index dfb927c..0000000 --- a/src/validators/fool/createFool.go +++ /dev/null @@ -1,14 +0,0 @@ -package validators - -import ( - "github.com/go-playground/validator/v10" -) - -type CreateFoolValidator struct { - Name string `json:"name" validate:"required"` -} - -func (c *CreateFoolValidator) Validate() error { - validate := validator.New() - return validate.Struct(c) -} diff --git a/src/validators/race/createRace.go b/src/validators/race/createRace.go deleted file mode 100644 index 9bd17bb..0000000 --- a/src/validators/race/createRace.go +++ /dev/null @@ -1,20 +0,0 @@ -package validators - -import "github.com/go-playground/validator/v10" - -type CreateRaceValidator struct { - Duration int `json:"duration" validate:"required,gte=0"` - ElapsedTime int `json:"elapsed_time" validate:"required,gte=0"` - Laps int `json:"laps" validate:"required,gte=0"` - RaceType string `json:"race_type" validate:"required,oneof=VS TIME_TRIAL"` - AverageSpeed int `json:"average_speed" validate:"required,gte=0"` - TotalFaults int `json:"total_faults" validate:"required,gte=0"` - EffectiveDuration int `json:"effective_duration" validate:"required,gte=0"` - UserID uint `json:"user_id" validate:"required"` - VehicleID uint `json:"vehicle_id" validate:"required"` -} - -func (c *CreateRaceValidator) Validate() error { - validate := validator.New() - return validate.Struct(c) -} diff --git a/src/validators/race/createRacesValidator.go b/src/validators/race/createRacesValidator.go new file mode 100644 index 0000000..8de6498 --- /dev/null +++ b/src/validators/race/createRacesValidator.go @@ -0,0 +1,22 @@ +package validators + +import ( + "api/src/models/attributes" + + "github.com/go-playground/validator/v10" +) + +type CreateRaceValidator struct { + Name string `json:"name" validate:"required"` + StartTime attributes.CustomTime `json:"start_time" validate:"required"` + EndTime *attributes.CustomTime `json:"end_time" validate:"omitempty,gtefield=StartTime"` + Status string `json:"status" validate:"required,oneof='not_started' 'in_progress' 'completed'"` + Type string `json:"type" validate:"required,oneof='manual' 'auto'"` + VehicleID uint `json:"vehicle_id" validate:"required"` +} + +func (c *CreateRaceValidator) Validate() error { + validate := validator.New() + + return validate.Struct(c) +} diff --git a/src/validators/race/updateRaceValidator.go b/src/validators/race/updateRaceValidator.go new file mode 100644 index 0000000..38fe80a --- /dev/null +++ b/src/validators/race/updateRaceValidator.go @@ -0,0 +1,18 @@ +package validators + +import ( + "api/src/models/attributes" + + "github.com/go-playground/validator/v10" +) + +type UpdateRaceValidator struct { + EndTime *attributes.CustomTime `json:"end_time"` + Name string `json:"name"` + Status string `json:"status" validate:"oneof='not_started' 'in_progress' 'completed' ''"` +} + +func (u *UpdateRaceValidator) Validate() error { + validate := validator.New() + return validate.Struct(u) +} diff --git a/src/validators/sensorData/createSensorData.go b/src/validators/sensorData/createSensorData.go deleted file mode 100644 index f1cd06a..0000000 --- a/src/validators/sensorData/createSensorData.go +++ /dev/null @@ -1,16 +0,0 @@ -package validators - -import ( - "github.com/go-playground/validator/v10" -) - -type CreateSensorDataValidator struct { - Light float64 `json:"light" validate:"required,gte=0"` - Sonar float64 `json:"sonar" validate:"required,gte=0"` - Track float64 `json:"track" validate:"required,gte=0"` -} - -func (c *CreateSensorDataValidator) Validate() error { - validate := validator.New() - return validate.Struct(c) -} diff --git a/src/validators/vehicle/createVehicle.go b/src/validators/vehicle/createVehicle.go deleted file mode 100644 index bada778..0000000 --- a/src/validators/vehicle/createVehicle.go +++ /dev/null @@ -1,28 +0,0 @@ -package validators - -import ( - "github.com/go-playground/validator/v10" -) - -type CreateVehicleValidator struct { - Name string `json:"vehicle_name" validate:"required"` - BatteryLife float64 `json:"battery_life" validate:"required,gte=0"` - LineSensor1 bool `json:"line_sensor1"` - LineSensor2 bool `json:"line_sensor2"` - LineSensor3 bool `json:"line_sensor3"` - Camera bool `json:"camera"` - SonarRange float64 `json:"sonar_range" validate:"required,gte=0"` - WheelPower1 int `json:"wheel_power1"` - WheelPower2 int `json:"wheel_power2"` - WheelPower3 int `json:"wheel_power3"` - WheelPower4 int `json:"wheel_power4"` - LedColor string `json:"led_color"` - DisplayPanel string `json:"display_panel"` - SpeakerStatus bool `json:"speaker_status"` - SoundPlaying string `json:"sound_playing"` -} - -func (c *CreateVehicleValidator) Validate() error { - validate := validator.New() - return validate.Struct(c) -} diff --git a/src/validators/vehicleState/createVehicleStateValidator.go b/src/validators/vehicleState/createVehicleStateValidator.go new file mode 100644 index 0000000..03e524d --- /dev/null +++ b/src/validators/vehicleState/createVehicleStateValidator.go @@ -0,0 +1,20 @@ +package validators + +import ( + "github.com/go-playground/validator/v10" +) + +type CreateVehicleStateValidator struct { + VehicleID uint `json:"vehicle_id" validate:"required"` + Face uint8 `json:"face" validate:"required,gte=0,lte=10"` + LedAnimation uint8 `json:"led_animation" validate:"required,gte=0,lte=5"` + BuzzerAlarm uint8 `json:"buzzer_alarm" validate:"required,gte=0,lte=1"` + VideoActivated uint8 `json:"video_activated" validate:"required,gte=0,lte=1"` + UserID uint `json:"user_id" validate:"required"` +} + +func (c *CreateVehicleStateValidator) Validate() error { + validate := validator.New() + + return validate.Struct(c) +} diff --git a/src/validators/vehicleState/updateVehicleStateValidator.go b/src/validators/vehicleState/updateVehicleStateValidator.go new file mode 100644 index 0000000..58601b3 --- /dev/null +++ b/src/validators/vehicleState/updateVehicleStateValidator.go @@ -0,0 +1,44 @@ +package validators + +import "github.com/go-playground/validator/v10" + +type PrimaryLedColorValidator struct { + LedIdentifier *int `json:"led_identifier" validate:"required,gte=0"` + Red *uint8 `json:"red" validate:"required,min=0,max=255"` + Green *uint8 `json:"green" validate:"required,min=0,max=255"` + Blue *uint8 `json:"blue" validate:"required,min=0,max=255"` +} + +type SecondaryLedColorValidator struct { + BinaryRepresentation *int `json:"binary_representation" validate:"required,gte=0"` + Red *uint8 `json:"red" validate:"required,min=0,max=255"` + Green *uint8 `json:"green" validate:"required,min=0,max=255"` + Blue *uint8 `json:"blue" validate:"required,min=0,max=255"` +} + +type BuzzerVariableValidator struct { + Activated *uint8 `json:"activated" validate:"required,oneof=0 1"` + Frequency *uint16 `json:"frequency" validate:"required,min=0,max=10000"` +} + +type HeadAngleValidator struct { + VerticalAngle *uint `json:"vertical_angle" validate:"required,min=0,max=180"` + HorizontalAngle *uint `json:"horizontal_angle" validate:"required,min=0,max=180"` +} + +type UpdateVehicleStateValidator struct { + Face *uint8 `json:"face" validate:"required,gte=0"` + LedAnimation *uint8 `json:"led_animation" validate:"required,gte=0,lte=5"` + BuzzerAlarm *uint8 `json:"buzzer_alarm" validate:"required,oneof=0 1"` + VideoActivated *uint8 `json:"video_activated" validate:"required,oneof=0 1"` + PrimaryLedColor *PrimaryLedColorValidator `json:"PrimaryLedColor" validate:"required"` + SecondaryLedColor *SecondaryLedColorValidator `json:"SecondaryLedColor" validate:"required"` + BuzzerVariable *BuzzerVariableValidator `json:"BuzzerVariable" validate:"required"` + HeadAngle *HeadAngleValidator `json:"HeadAngle" validate:"required"` +} + +func (u *UpdateVehicleStateValidator) Validate() error { + validate := validator.New() + + return validate.Struct(u) +}