diff --git a/internal/api/api.go b/internal/api/api.go index 7c3c248..13d240a 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -13,12 +13,16 @@ type API struct { } func (api *API) Initialize() { + validator := validator.New(validator.WithRequiredStructEnabled()) + validator.RegisterValidation("password", PasswordValidator) + db := &models.DB{} ech := echo.New() - ech.Validator = &CustomValidator{validator: validator.New(validator.WithRequiredStructEnabled())} + ech.Validator = &CustomValidator{validator: validator} db.Connect() db.MigrateClients() + db.MigrateUsers() api.instance = ech api.db = db diff --git a/internal/api/client.go b/internal/api/client.go index dd4a9bb..8596ac0 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -6,12 +6,12 @@ import ( "net/http" "strconv" - "github.com/hazemKrimi/crimson-vault/internal/models" + "github.com/hazemKrimi/crimson-vault/internal/types" "github.com/labstack/echo/v4" ) func (api *API) CreateClientHandler(context echo.Context) error { - var body models.CreateClientRequestBody + var body types.CreateClientRequestBody if err := context.Bind(&body); err != nil { log.Println(fmt.Sprintf("Error creating Client: %v.", err)) @@ -52,13 +52,13 @@ func (api *API) GetClientHandler(context echo.Context) error { return context.String(http.StatusInternalServerError, "Unexpected error getting Client!") } - var client models.Client + var client types.Client if err := api.db.GetClient(id, &client); err != nil { return context.String(http.StatusNotFound, "Client not found!") } - log.Println(fmt.Sprintf("Got client with ID %d.", client.ID)) + log.Println(fmt.Sprintf("Got User with ID %d.", client.ID)) return context.JSON(http.StatusOK, client) } @@ -75,7 +75,7 @@ func (api *API) UpdateClientHandler(context echo.Context) error { return context.String(http.StatusInternalServerError, "Unexpected error updating Client!") } - var body models.UpdateClientRequestBody + var body types.UpdateClientRequestBody if err := context.Bind(&body); err != nil { log.Println(fmt.Sprintf("Error updating Client: %v.", err)) @@ -86,13 +86,13 @@ func (api *API) UpdateClientHandler(context echo.Context) error { return err } - var client models.Client + var client types.Client if err := api.db.UpdateClient(id, body, &client); err != nil { return context.String(http.StatusNotFound, "Client not found!") } - log.Println(fmt.Sprintf("Updated client with ID %d.", client.ID)) + log.Println(fmt.Sprintf("Updated Client with ID %d.", client.ID)) return context.JSON(http.StatusOK, client) } @@ -109,12 +109,12 @@ func (api *API) DeleteClientHandler(context echo.Context) error { return context.String(http.StatusInternalServerError, "Unexpected error deleting Client!") } - var client models.Client + var client types.Client if err := api.db.DeleteClient(id); err != nil { return context.String(http.StatusNotFound, "Client not found!") } - log.Println(fmt.Sprintf("Deleted client with ID %d.", client.ID)) + log.Println(fmt.Sprintf("Deleted Client with ID %d.", client.ID)) return context.String(http.StatusOK, "Client deleted successfully!") } diff --git a/internal/api/routes.go b/internal/api/routes.go index 7f26f37..c0d901a 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -1,11 +1,23 @@ package api +import "github.com/labstack/echo/v4/middleware" + func (api *API) ClientRoutes() { - group := api.instance.Group("/clients") + clients := api.instance.Group("/clients") + users := api.instance.Group("/users") - group.GET("/", api.GetAllClientsHandler) - group.POST("/", api.CreateClientHandler) - group.GET("/:id", api.GetClientHandler) - group.PUT("/:id", api.UpdateClientHandler) - group.DELETE("/:id", api.DeleteClientHandler) + clients.GET("/", api.GetAllClientsHandler) + clients.POST("/", api.CreateClientHandler) + clients.GET("/:id", api.GetClientHandler) + clients.PUT("/:id", api.UpdateClientHandler) + clients.DELETE("/:id", api.DeleteClientHandler) + + users.GET("/", api.GetAllUsersHandler) + users.POST("/", api.CreateUserHandler) + users.GET("/:id", api.GetUserHandler) + users.PUT("/:id", api.UpdateUserHandler) + users.PUT("/:id/security", api.UpdateUserSecurityDetailsHandler) + users.PUT("/:id/logo", api.UpdateUserLogoHandler, middleware.BodyLimit("2M")) + users.DELETE("/:id", api.DeleteUserHandler) + users.DELETE("/:id/logo", api.DeleteUserLogoHandler) } diff --git a/internal/api/user.go b/internal/api/user.go new file mode 100644 index 0000000..f8f8242 --- /dev/null +++ b/internal/api/user.go @@ -0,0 +1,279 @@ +package api + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/hazemKrimi/crimson-vault/internal/types" + "github.com/labstack/echo/v4" +) + +func (api *API) CreateUserHandler(context echo.Context) error { + var body types.CreateUserRequestBody + + if err := context.Bind(&body); err != nil { + log.Println(fmt.Sprintf("Error creating User: %v.", err)) + return context.String(http.StatusBadRequest, "Invalid JSON!") + } + + if err := context.Validate(body); err != nil { + return err + } + + user := api.db.CreateUser(body) + + log.Println(fmt.Sprintf("User created with ID %d.", user.ID)) + return context.JSON(http.StatusOK, user) +} + +func (api *API) GetAllUsersHandler(context echo.Context) error { + users, err := api.db.GetUsers() + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error getting User!") + } + + log.Println("Got all Users.") + return context.JSON(http.StatusOK, users) +} + +func (api *API) GetUserHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to get a User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error getting User!") + } + + var user types.User + + if err := api.db.GetUser(id, &user); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + log.Println(fmt.Sprintf("Got User with ID %d.", user.ID)) + return context.JSON(http.StatusOK, user) +} + +func (api *API) UpdateUserHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to update a User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error updating User!") + } + + var body types.UpdateUserRequestBody + + if err := context.Bind(&body); err != nil { + log.Println(fmt.Sprintf("Error updating User: %v.", err)) + return context.String(http.StatusBadRequest, "Invalid JSON!") + } + + if err := context.Validate(body); err != nil { + return err + } + + var user types.User + + if err := api.db.UpdateUser(id, body, &user); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + log.Println(fmt.Sprintf("Updated user with ID %d.", user.ID)) + return context.JSON(http.StatusOK, user) +} + +func (api *API) UpdateUserSecurityDetailsHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to create security details for a User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error while creating security details for User!") + } + + var body types.UpdateUserSecurityDetailsBody + + if err := context.Bind(&body); err != nil { + log.Println(fmt.Sprintf("Error creating security details for User: %v.", err)) + return context.String(http.StatusBadRequest, "Invalid JSON!") + } + + if err := context.Validate(body); err != nil { + return err + } + + var user types.User + + if err := api.db.UpdateUserSecurityDetails(id, body, &user); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + log.Println(fmt.Sprintf("Updated security details of user with ID %d.", user.ID)) + return context.JSON(http.StatusOK, user) +} + +func (api *API) UpdateUserLogoHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to update logo for User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error updating logo for User!") + } + + var user types.User + + if err := api.db.GetUser(id, &user); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + if user.Username == "" { + return context.String(http.StatusBadRequest, "You have to add a username first for this User!") + } + + file, err := context.FormFile("logo") + + if err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + ext := strings.ToLower(filepath.Ext(file.Filename)) + allowedExtensions := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".bmp": true, + ".webp": true, + } + + if !allowedExtensions[ext] { + return context.String(http.StatusBadRequest, "Invalid file type, only image files are allowed!") + } + + src, err := file.Open() + + if err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + defer src.Close() + + data, err := io.ReadAll(src) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + filetype := http.DetectContentType(data) + + if !strings.HasPrefix(filetype, "image/") { + return context.String(http.StatusBadRequest, "Uploaded file is not a valid image!") + } + + err = os.MkdirAll(user.Username, os.ModePerm) + + if err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + path, err := filepath.Abs(filepath.Join(user.Username, fmt.Sprintf("logo%s", ext))) + + if err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + if err := os.WriteFile(path, data, 0644); err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + if err := api.db.UpdateUserLogo(path, &user); err != nil { + log.Println(fmt.Sprintf("Error updating logo for User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error while updating logo for User!") + } + + return context.JSON(http.StatusOK, user) +} + +func (api *API) DeleteUserHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to delete a User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error deleting User!") + } + + if err := api.db.DeleteUser(id); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + log.Println(fmt.Sprintf("Deleted User with ID %d.", id)) + return context.String(http.StatusOK, "User deleted successfully!") +} + +func (api *API) DeleteUserLogoHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to delete logo of User!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + log.Println(fmt.Sprintf("Error deleting logo of User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error deleting logo of User!") + } + + var user types.User + + if err := api.db.GetUser(id, &user); err != nil { + return context.String(http.StatusNotFound, "User not found!") + } + + os.Remove(user.Logo) + + if err := api.db.DeleteUserLogo(&user); err != nil { + log.Println(fmt.Sprintf("Error deleting logo of User: %v.", err)) + return context.String(http.StatusInternalServerError, "Unexpected error deleting logo of User!") + } + + log.Println(fmt.Sprintf("Deleted logo of User with ID %d.", user.ID)) + return context.String(http.StatusOK, "User logo deleted successfully!") +} diff --git a/internal/api/validator.go b/internal/api/validator.go index 6bfa34b..6af1eab 100644 --- a/internal/api/validator.go +++ b/internal/api/validator.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "regexp" "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" @@ -17,3 +18,21 @@ func (validator *CustomValidator) Validate(i any) error { } return nil } + +func PasswordValidator(fieldLevel validator.FieldLevel) bool { + password := fieldLevel.Field().String() + + var ( + upper = regexp.MustCompile(`[A-Z]`) + lower = regexp.MustCompile(`[a-z]`) + number = regexp.MustCompile(`[0-9]`) + special = regexp.MustCompile(`[!@#~$%^&*()+|_{}:<>?,./;'\[\]\\-]`) + minChars = 8 + ) + + return len(password) >= minChars && + upper.MatchString(password) && + lower.MatchString(password) && + number.MatchString(password) && + special.MatchString(password) +} diff --git a/internal/models/client.go b/internal/models/client.go index f19a861..76477ed 100644 --- a/internal/models/client.go +++ b/internal/models/client.go @@ -1,58 +1,30 @@ package models import ( - "time" - - "gorm.io/gorm" + "github.com/hazemKrimi/crimson-vault/internal/types" ) -type Client struct { - ID uint32 `json:"id"` - CreatedAt time.Time `json:"createAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `json:"deletedAt" gorm:"index"` - Name string `json:"name"` - FiscalCode string `json:"fiscalCode"` - Address string `json:"address"` - Zip string `json:"zip"` - Country string `json:"country"` - Phone string `json:"phone"` - Email string `json:"email"` -} - -type CreateClientRequestBody struct { - Name string `json:"name" validate:"required"` - FiscalCode string `json:"fiscalCode"` - Address string `json:"address" validate:"required"` - Zip string `json:"zip" validate:"required"` - Country string `json:"country" validate:"required"` - Phone string `json:"phone" validate:"required,e164"` - Email string `json:"email" validate:"required,email"` -} - -type UpdateClientRequestBody struct { - Name string `json:"name"` - FiscalCode string `json:"fiscalCode"` - Address string `json:"address"` - Zip string `json:"zip"` - Country string `json:"country"` - Phone string `json:"phone" validate:"omitempty,e164"` - Email string `json:"email" validate:"omitempty,email"` -} - func (db *DB) MigrateClients() { - db.instance.AutoMigrate(&Client{}) + db.instance.AutoMigrate(&types.Client{}) } -func (db *DB) CreateClient(body CreateClientRequestBody) Client { - client := Client{Name: body.Name, Country: body.Country, Phone: body.Phone} +func (db *DB) CreateClient(body types.CreateClientRequestBody) types.Client { + client := types.Client{ + Name: body.Name, + FiscalCode: body.FiscalCode, + Address: body.Address, + Zip: body.Zip, + Country: body.Country, + Phone: body.Phone, + Email: body.Email, + } db.instance.Create(&client) return client } -func (db *DB) GetClients() ([]Client, error) { - var clients []Client +func (db *DB) GetClients() ([]types.Client, error) { + var clients []types.Client result := db.instance.Find(&clients) @@ -63,8 +35,8 @@ func (db *DB) GetClients() ([]Client, error) { return clients, nil } -func (db *DB) GetClient(id int, client *Client) error { - result := db.instance.Where("id = ?", id).First(&client, id) +func (db *DB) GetClient(id int, client *types.Client) error { + result := db.instance.Where("id = ?", id).First(client, id) if result.Error != nil { return result.Error @@ -73,21 +45,21 @@ func (db *DB) GetClient(id int, client *Client) error { return nil } -func (db *DB) UpdateClient(id int, body UpdateClientRequestBody, client *Client) error { - result := db.instance.Where("id = ?", id).First(&client, id) +func (db *DB) UpdateClient(id int, body types.UpdateClientRequestBody, client *types.Client) error { + result := db.instance.Where("id = ?", id).First(client, id) if result.Error != nil { return result.Error } - result = db.instance.Model(&client).Updates(Client{ - Name: body.Name, + result = db.instance.Model(client).Updates(types.Client{ + Name: body.Name, FiscalCode: body.FiscalCode, - Address: body.Address, - Zip: body.Zip, - Country: body.Country, - Phone: body.Phone, - Email: body.Email, + Address: body.Address, + Zip: body.Zip, + Country: body.Country, + Phone: body.Phone, + Email: body.Email, }) if result.Error != nil { @@ -98,7 +70,7 @@ func (db *DB) UpdateClient(id int, body UpdateClientRequestBody, client *Client) } func (db *DB) DeleteClient(id int) error { - result := db.instance.Delete(&Client{}, id) + result := db.instance.Delete(&types.Client{}, id) if result.Error != nil { return result.Error diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..fc58a2b --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,123 @@ +package models + +import ( + "github.com/hazemKrimi/crimson-vault/internal/types" +) + +func (db *DB) MigrateUsers() { + db.instance.AutoMigrate(&types.User{}) +} + +func (db *DB) CreateUser(body types.CreateUserRequestBody) types.User { + user := types.User{ + Name: body.Name, + FiscalCode: body.FiscalCode, + Address: body.Address, + Zip: body.Zip, + Country: body.Country, + Phone: body.Phone, + Email: body.Email, + } + + db.instance.Create(&user) + return user +} + +func (db *DB) GetUsers() ([]types.User, error) { + var users []types.User + + result := db.instance.Find(&users) + + if result.Error != nil { + return nil, result.Error + } + + return users, nil +} + +func (db *DB) GetUser(id int, user *types.User) error { + result := db.instance.Where("id = ?", id).First(user, id) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateUser(id int, body types.UpdateUserRequestBody, user *types.User) error { + result := db.instance.Where("id = ?", id).First(user, id) + + if result.Error != nil { + return result.Error + } + + result = db.instance.Model(user).Updates(types.User{ + Name: body.Name, + FiscalCode: body.FiscalCode, + Address: body.Address, + Zip: body.Zip, + Country: body.Country, + Phone: body.Phone, + Email: body.Email, + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateUserSecurityDetails(id int, body types.UpdateUserSecurityDetailsBody, user *types.User) error { + result := db.instance.Where("id = ?", id).First(user, id) + + if result.Error != nil { + return result.Error + } + + result = db.instance.Model(user).Updates(types.User{ + Username: body.Username, + Password: body.Password, + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateUserLogo(path string, user *types.User) error { + result := db.instance.Model(user).Updates(types.User{ + Logo: path, + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) DeleteUser(id int) error { + result := db.instance.Delete(&types.User{}, id) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) DeleteUserLogo(user *types.User) error { + result := db.instance.Model(user).Updates(&types.User{ + Logo: "", + }) + + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/internal/types/client.go b/internal/types/client.go new file mode 100644 index 0000000..851dfcd --- /dev/null +++ b/internal/types/client.go @@ -0,0 +1,41 @@ +package types + +import ( + "time" + + "gorm.io/gorm" +) + +type Client struct { + ID uint32 `json:"id"` + CreatedAt time.Time `json:"createAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"deletedAt" gorm:"index"` + Name string `json:"name"` + FiscalCode string `json:"fiscalCode"` + Address string `json:"address"` + Zip string `json:"zip"` + Country string `json:"country"` + Phone string `json:"phone"` + Email string `json:"email"` +} + +type CreateClientRequestBody struct { + Name string `json:"name" validate:"required"` + FiscalCode string `json:"fiscalCode"` + Address string `json:"address" validate:"required"` + Zip string `json:"zip" validate:"required"` + Country string `json:"country" validate:"required"` + Phone string `json:"phone" validate:"required,e164"` + Email string `json:"email" validate:"required,email"` +} + +type UpdateClientRequestBody struct { + Name string `json:"name"` + FiscalCode string `json:"fiscalCode"` + Address string `json:"address"` + Zip string `json:"zip"` + Country string `json:"country"` + Phone string `json:"phone" validate:"omitempty,e164"` + Email string `json:"email" validate:"omitempty,email"` +} diff --git a/internal/types/user.go b/internal/types/user.go new file mode 100644 index 0000000..567c3d1 --- /dev/null +++ b/internal/types/user.go @@ -0,0 +1,50 @@ +package types + +import ( + "time" + + "gorm.io/gorm" +) + +type User struct { + ID uint32 `json:"id"` + CreatedAt time.Time `json:"createAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"deletedAt" gorm:"index"` + Logo string `json:"logo"` + Name string `json:"name"` + FiscalCode string `json:"fiscalCode"` + Address string `json:"address"` + Zip string `json:"zip"` + Country string `json:"country"` + Phone string `json:"phone"` + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password"` +} + +type CreateUserRequestBody struct { + Name string `json:"name" validate:"required"` + FiscalCode string `json:"fiscalCode" validate:"required"` + Address string `json:"address" validate:"required"` + Zip string `json:"zip" validate:"required"` + Country string `json:"country" validate:"required"` + Phone string `json:"phone" validate:"required,e164"` + Email string `json:"email" validate:"required,email"` +} + +type UpdateUserRequestBody struct { + Name string `json:"name"` + FiscalCode string `json:"fiscalCode"` + Address string `json:"address"` + Zip string `json:"zip"` + Country string `json:"country"` + Phone string `json:"phone" validate:"omitempty,e164"` + Email string `json:"email" validate:"omitempty,email"` +} + +type UpdateUserSecurityDetailsBody struct { + Username string `json:"username"` + Password string `json:"password" validate:"password"` + ConfirmPassword string `json:"confirmPassword" validate:"password,eqcsfield=Password"` +}