diff --git a/Dockerfile b/Dockerfile index e432c5d..bc6efe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ WORKDIR /root/ COPY --from=builder /app/cmd/api/main . COPY config /root/config +COPY translations /root/translations EXPOSE 8080 diff --git a/cmd/api/main.go b/cmd/api/main.go index 645e3b3..6cfc6a8 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -12,6 +12,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/db" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" ) @@ -20,14 +21,15 @@ func main() { db.Load() service.Init() + translations.Init("./translations") gin.SetMode(config.Router.GinMode) router := gin.New() - router.Use(gin.Logger(), gin.Recovery(), middleware.GetAPIKey) + router.Use(gin.Logger(), gin.Recovery(), middleware.DetectLanguage, middleware.GetAPIKey) - router.GET("/healthz", func(ctx *gin.Context) { - ctx.Writer.WriteString("Hello, world!") + router.GET("/healthcare", func(ctx *gin.Context) { + ctx.Writer.WriteString(ctx.Keys["lang"].(translations.Translation).Hello) }) v1.NewRouter(router) diff --git a/internal/controller/http/v1/apikey.go b/internal/controller/http/v1/apikey.go index 280871e..a47b478 100644 --- a/internal/controller/http/v1/apikey.go +++ b/internal/controller/http/v1/apikey.go @@ -11,6 +11,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/utils" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" "gorm.io/gorm" ) @@ -26,15 +27,17 @@ func newAPIKeyRoutes(handler *gin.RouterGroup) { specific := h.Group("/:key") specific.Use(func(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + key, err := service.APIKey.Find(ctx.Param("key")) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "No API key found."}) + ctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": dict.NoAPIKeyFound}) return } log.Print(err) - ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -49,6 +52,8 @@ func newAPIKeyRoutes(handler *gin.RouterGroup) { } func GetAllAPIKeys(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var ( page int = 1 perpage int = 10 @@ -65,7 +70,7 @@ func GetAllAPIKeys(ctx *gin.Context) { keys, err := service.APIKey.FindAll(page, perpage) if err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -73,6 +78,8 @@ func GetAllAPIKeys(ctx *gin.Context) { } func GetMyAPIKeys(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch) var ( @@ -90,7 +97,7 @@ func GetMyAPIKeys(ctx *gin.Context) { keys, err := service.APIKey.FindByOwnerID(issuer.ID, page, perpage) if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -98,6 +105,8 @@ func GetMyAPIKeys(ctx *gin.Context) { } func CreateAPIKey(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var issuerID uint = 0 if u, exists := ctx.Get("auth_user"); exists { issuerID = u.(*dto.ProfileSearch).ID @@ -111,7 +120,7 @@ func CreateAPIKey(ctx *gin.Context) { newKey, err := service.APIKey.Create(issuerID, uint(maxUsage)) if err != nil { log.Printf("Error generating new API key: %v", err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't generate new key"}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -119,15 +128,17 @@ func CreateAPIKey(ctx *gin.Context) { } func GetAPIKeyInfo(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + key, err := service.APIKey.Find(ctx.Param("key")) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "No API key found."}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.NoAPIKeyFound}) return } log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -135,27 +146,31 @@ func GetAPIKeyInfo(ctx *gin.Context) { } func DestroyAPIKey(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + if err := service.APIKey.Delete(ctx.Keys["api_key_lookup"].(*model.APIKey).Key); err != nil { log.Printf("Error destroying API key: %v", err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't destroy key"}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": "Destroyied key"}) + ctx.JSON(http.StatusOK, gin.H{"message": gin.H{"error": dict.APIKeyDestroyed}}) } func UpdateAPIKey(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + key := ctx.Keys["api_key_lookup"].(*model.APIKey) var body dto.APIKey if err := ctx.BindJSON(&body); err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.InvalidBody}) return } if errs := utils.ValidateStruct(&body); errs != nil { - err := utils.ValidateErrorMessage(errs[0]) + err := utils.ValidateErrorMessage(ctx, errs[0]) log.Print(err) ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{err.Param: err.Message}}) @@ -164,9 +179,9 @@ func UpdateAPIKey(ctx *gin.Context) { if err := service.APIKey.Update(key.Key, &body); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": "API key updated"}) + ctx.JSON(http.StatusOK, gin.H{"message": dict.APIKeyUpdated}) } diff --git a/internal/controller/http/v1/auth.go b/internal/controller/http/v1/auth.go index a8ec3c8..8a5d8d3 100644 --- a/internal/controller/http/v1/auth.go +++ b/internal/controller/http/v1/auth.go @@ -10,6 +10,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/utils" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgconn" "golang.org/x/crypto/bcrypt" @@ -28,30 +29,32 @@ func newAuthRoutes(handler *gin.RouterGroup) { } func RegisterHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var body dto.UserRegister if err := ctx.BindJSON(&body); err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.InvalidBody}) return } if errs := utils.ValidateStruct(&body); errs != nil { - err := utils.ValidateErrorMessage(errs[0]) + err := utils.ValidateErrorMessage(ctx, errs[0]) log.Print(err) ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{err.Param: err.Message}}) return } - user, err := service.User.CreateUser(body.FirstName, body.LastName, body.Username, body.Email, body.Password) + user, err := service.User.CreateUser(ctx, body.FirstName, body.LastName, body.Username, body.Email, body.Password) if err == nil { if token, err := utils.GenerateJWT(user.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) } else { - ctx.JSON(http.StatusOK, gin.H{"message": "User created", "token": token}) + ctx.JSON(http.StatusOK, gin.H{"token": token}) } return @@ -67,24 +70,26 @@ func RegisterHandler(ctx *gin.Context) { var pgErr *pgconn.PgError // 23505 => duplicated key value violates unique constraint if errors.Is(err, gorm.ErrDuplicatedKey) || (errors.As(err, &pgErr) && pgErr.Code == "23505") { - ctx.JSON(http.StatusUnauthorized, gin.H{"error": "An user with that username or email already exists."}) + ctx.JSON(http.StatusUnauthorized, gin.H{"error": dict.AuthDuplicatedUser}) return } - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) } func LoginHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var body dto.UserLogin if err := ctx.BindJSON(&body); err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.InvalidBody}) return } if errs := utils.ValidateStruct(&body); errs != nil { - err := utils.ValidateErrorMessage(errs[0]) + err := utils.ValidateErrorMessage(ctx, errs[0]) log.Print(err) ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{err.Param: err.Message}}) @@ -97,11 +102,11 @@ func LoginHandler(ctx *gin.Context) { log.Print(err) if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusUnauthorized, gin.H{"error": "No user found with that username or email"}) + ctx.JSON(http.StatusUnauthorized, gin.H{"error": dict.AuthWrongCredentials}) return } - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -109,36 +114,38 @@ func LoginHandler(ctx *gin.Context) { log.Print(err) if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { - ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Password mismatch"}) + ctx.JSON(http.StatusUnauthorized, gin.H{"error": dict.AuthWrongCredentials}) return } - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } if token, err := utils.GenerateJWT(user.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) } else { - ctx.JSON(http.StatusOK, gin.H{"message": "User logged in", "token": token}) + ctx.JSON(http.StatusOK, gin.H{"token": token}) } } func UpdateUserHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch) var body dto.UserUpdate if err := ctx.BindJSON(&body); err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.InvalidBody}) return } if errs := utils.ValidateStruct(&body); errs != nil { - err := utils.ValidateErrorMessage(errs[0]) + err := utils.ValidateErrorMessage(ctx, errs[0]) log.Print(err) ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{err.Param: err.Message}}) @@ -147,21 +154,21 @@ func UpdateUserHandler(ctx *gin.Context) { if body.Username != "" { if profile, err := service.User.GetByUsername(body.Username); profile != nil && err == nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "An user with that username already exists"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.AuthDuplicatedUser}) return } } if body.Email != "" { if profile, err := service.User.GetByEmail(body.Email); profile != nil && err == nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "An user with that email already exists"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.AuthDuplicatedUser}) return } } if body.Password != "" { if hashedPassword, err := bcrypt.GenerateFromPassword([]byte(body.Password), config.Security.BcryptCost); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) } else { body.Password = string(hashedPassword) } @@ -169,26 +176,23 @@ func UpdateUserHandler(ctx *gin.Context) { if err := service.User.Update(issuer.ID, &body); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": "User updated"}) + ctx.JSON(http.StatusOK, gin.H{"message": dict.AuthUserUpdated}) } func DeleteUserHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch) if err := service.User.DeleteUser(issuer.ID); err != nil { log.Print(err) - if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "User not found."}) - return - } - - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": "User deleted"}) + ctx.JSON(http.StatusOK, gin.H{"message": dict.AuthUserDeleted}) } diff --git a/internal/controller/http/v1/search.go b/internal/controller/http/v1/search.go index 94f09d4..d37bdbc 100644 --- a/internal/controller/http/v1/search.go +++ b/internal/controller/http/v1/search.go @@ -10,6 +10,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" "gorm.io/gorm" ) @@ -23,10 +24,12 @@ func newSearchRoutes(handler *gin.RouterGroup) { } func SearchByNameHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + name := ctx.Query("name") if !regexp.MustCompile(`[a-zA-Z ]`).MatchString(name) || strings.TrimSpace(name) == "" { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Searches cannot be composed of spaces only or special characters"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.SearchIncorrect}) return } @@ -46,12 +49,12 @@ func SearchByNameHandler(ctx *gin.Context) { users, err := service.User.GetBySimilarName(name, page, perpage) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "No user found with that name."}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.SearchNoResults}) return } log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } diff --git a/internal/controller/http/v1/user.go b/internal/controller/http/v1/user.go index 5c8bab5..859d5b2 100644 --- a/internal/controller/http/v1/user.go +++ b/internal/controller/http/v1/user.go @@ -12,6 +12,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/utils" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" "gorm.io/gorm" ) @@ -32,6 +33,8 @@ func newUserRoutes(handler *gin.RouterGroup) { } func GetProfileHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var issuer *dto.ProfileSearch = nil if p, exists := ctx.Get("auth_user"); exists { issuer = p.(*dto.ProfileSearch) @@ -42,7 +45,7 @@ func GetProfileHandler(ctx *gin.Context) { if err == nil { if !utils.HasPermissionsByContext(ctx, config.Permissions.ManageUser) { if user.Show.Profile == -1 && (issuer == nil || issuer.ID != user.ID) { - ctx.JSON(http.StatusForbidden, gin.H{"error": "User disabled viewing their profile"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": dict.UserDisabledProfile}) return } } @@ -55,14 +58,16 @@ func GetProfileHandler(ctx *gin.Context) { log.Print(err) if err == gorm.ErrRecordNotFound { - ctx.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.UserNotFound}) return } - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) } func GetFollowersHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var issuer *dto.ProfileSearch = nil if p, exists := ctx.Get("auth_user"); exists { issuer = p.(*dto.ProfileSearch) @@ -72,23 +77,23 @@ func GetFollowersHandler(ctx *gin.Context) { user, err := service.User.GetByUsername(username) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "No user found with that username."}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.UserNotFound}) return } log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } if !utils.HasPermissionsByContext(ctx, config.Permissions.ManageUser) { if user.Show.Profile == -1 && (issuer == nil || issuer.ID != user.ID) { - ctx.JSON(http.StatusForbidden, gin.H{"error": "User disabled viewing their profile"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": dict.UserDisabledProfile}) return } if user.Show.Followers == -1 && (issuer == nil || issuer.ID != user.ID) { - ctx.JSON(http.StatusForbidden, gin.H{"error": "User disabled viewing whom are following them"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": dict.UserDisabledFollowers}) return } } @@ -109,7 +114,7 @@ func GetFollowersHandler(ctx *gin.Context) { pagination, err := service.Follow.GetFollowers(user.ID, page, perpage) if err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -117,6 +122,8 @@ func GetFollowersHandler(ctx *gin.Context) { } func GetFollowingHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + var issuer *dto.ProfileSearch = nil if p, exists := ctx.Get("auth_user"); exists { issuer = p.(*dto.ProfileSearch) @@ -126,23 +133,23 @@ func GetFollowingHandler(ctx *gin.Context) { user, err := service.User.GetByUsername(username) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "No user found with that username."}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.UserNotFound}) return } log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } if !utils.HasPermissionsByContext(ctx, config.Permissions.ManageUser) { if user.Show.Profile == -1 && (issuer == nil || issuer.ID != user.ID) { - ctx.JSON(http.StatusForbidden, gin.H{"error": "User disabled viewing their profile"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": dict.UserDisabledProfile}) return } if user.Show.Following == -1 && (issuer == nil || issuer.ID != user.ID) { - ctx.JSON(http.StatusForbidden, gin.H{"error": "User disabled viewing whom they are following"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": dict.UserDisabledFollowers}) return } } @@ -163,7 +170,7 @@ func GetFollowingHandler(ctx *gin.Context) { pagination, err := service.Follow.GetFollowing(user.ID, page, perpage) if err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -171,23 +178,25 @@ func GetFollowingHandler(ctx *gin.Context) { } func GetUserPermissions(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + username := ctx.Param("username") user, err := service.User.GetByUsername(username) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "No user found with that username."}) + ctx.JSON(http.StatusNotFound, gin.H{"error": dict.UserNotFound}) return } log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } permissions, err := service.Permission.GetPermissions(user.ID) if err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } @@ -201,67 +210,71 @@ func GetUserPermissions(ctx *gin.Context) { } func FollowUserHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch) receiver, err := service.User.GetByUsername(ctx.Param("username")) if err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.UserNotFound}) return } if issuer.ID == receiver.ID { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Users cannot follow or unfollow themselves"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.UserErrorFollowItself}) return } if exists, err := service.Follow.Exists(receiver.ID, issuer.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } else if exists { - ctx.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("Already following %s", receiver.Username)}) + ctx.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf(dict.UserFollowingAlready, receiver.Username)}) return } if err := service.Follow.FollowUser(receiver.ID, issuer.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Started following %s", receiver.Username)}) + ctx.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf(dict.UserFollowingStarted, receiver.Username)}) } func UnfollowUserHandler(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch) receiver, err := service.User.GetByUsername(ctx.Param("username")) if err != nil { log.Print(err) - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.UserNotFound}) return } if issuer.ID == receiver.ID { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Users cannot follow or unfollow themselves"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": dict.UserErrorFollowItself}) return } if exists, err := service.Follow.Exists(receiver.ID, issuer.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } else if !exists { - ctx.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("Not following %s", receiver.Username)}) + ctx.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf(dict.UserFollowingNot, receiver.Username)}) return } if err := service.Follow.UnfollowUser(receiver.ID, issuer.ID); err != nil { log.Print(err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": dict.InternalServerError}) return } - ctx.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Stopped following %s", receiver.Username)}) + ctx.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf(dict.UserFollowingStopped, receiver.Username)}) } diff --git a/internal/service/usecase/user.go b/internal/service/usecase/user.go index 51a7c5e..65f2967 100644 --- a/internal/service/usecase/user.go +++ b/internal/service/usecase/user.go @@ -6,6 +6,7 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/model/dto" "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service/repository" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/utils" + "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) @@ -18,7 +19,7 @@ func NewUserUseCase() UserUseCase { return UserUseCase{ur: repository.NewUserRepository()} } -func (uuc UserUseCase) CreateUser(firstname, lastname, username, email, password string) (*model.User, error) { +func (uuc UserUseCase) CreateUser(ctx *gin.Context, firstname, lastname, username, email, password string) (*model.User, error) { newUser := model.User{ FirstName: firstname, LastName: lastname, @@ -28,7 +29,7 @@ func (uuc UserUseCase) CreateUser(firstname, lastname, username, email, password } if errs := utils.ValidateStruct(&newUser); errs != nil { - return nil, utils.ValidateErrorMessage(errs[0]) + return nil, utils.ValidateErrorMessage(ctx, errs[0]) } if _, err := uuc.GetByUsernameOrEmail(username, email); err == nil { diff --git a/pkg/middleware/apikey.go b/pkg/middleware/apikey.go index 848cb35..9a13a0a 100644 --- a/pkg/middleware/apikey.go +++ b/pkg/middleware/apikey.go @@ -7,13 +7,16 @@ import ( "github.com/devkcud/arkhon-foundation/arkhon-api/internal/model" "github.com/devkcud/arkhon-foundation/arkhon-api/internal/service" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" "github.com/gin-gonic/gin" ) func GetAPIKey(ctx *gin.Context) { + dict := translations.GetTranslation(ctx) + key, err := service.APIKey.Find(ctx.GetHeader("X-API-KEY")) if err != nil { - ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"}) + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": dict.InvalidAPIKey}) return } @@ -21,11 +24,13 @@ func GetAPIKey(ctx *gin.Context) { ctx.Next() } -func apiKeyHas(ctx *gin.Context, b int, field string) { +func apiKeyHas(ctx *gin.Context, b int, permission string) { + dict := translations.GetTranslation(ctx) + key := ctx.Keys["api_key"].(*model.APIKey) if key.MaxUsage != 0 && key.TimesUsed >= key.MaxUsage { - ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "API key has reached its maximum allowed usage. Please contact support or obtain a new key."}) + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": dict.MaximumAPIKey}) return } @@ -34,7 +39,7 @@ func apiKeyHas(ctx *gin.Context, b int, field string) { } if b == -1 { - ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("This API key doesn't have the permission to handle %s", field)}) + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf(dict.RequirePermissionAPIKey, permission)}) return } @@ -43,25 +48,25 @@ func apiKeyHas(ctx *gin.Context, b int, field string) { func APIKeyHasEnabledKeyManage(ctx *gin.Context) { key := ctx.Keys["api_key"].(*model.APIKey) - apiKeyHas(ctx, key.EnabledKeyManage, "api key manipulation") + apiKeyHas(ctx, key.EnabledKeyManage, "manage.api") } func APIKeyHasEnabledAuth(ctx *gin.Context) { key := ctx.Keys["api_key"].(*model.APIKey) - apiKeyHas(ctx, key.EnabledAuth, "auth") + apiKeyHas(ctx, key.EnabledAuth, "manage.auth") } func APIKeyHasEnabledSearch(ctx *gin.Context) { key := ctx.Keys["api_key"].(*model.APIKey) - apiKeyHas(ctx, key.EnabledSearch, "searches") + apiKeyHas(ctx, key.EnabledSearch, "query.search") } func APIKeyHasEnabledUserFetch(ctx *gin.Context) { key := ctx.Keys["api_key"].(*model.APIKey) - apiKeyHas(ctx, key.EnabledUserFetch, "user fetch") + apiKeyHas(ctx, key.EnabledUserFetch, "query.user") } func APIKeyHasEnabledUserActions(ctx *gin.Context) { key := ctx.Keys["api_key"].(*model.APIKey) - apiKeyHas(ctx, key.EnabledUserActions, "user actions") + apiKeyHas(ctx, key.EnabledUserActions, "actions") } diff --git a/pkg/middleware/language.go b/pkg/middleware/language.go new file mode 100644 index 0000000..11c1d3c --- /dev/null +++ b/pkg/middleware/language.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "slices" + "strings" + + "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/language" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" + "github.com/gin-gonic/gin" +) + +func DetectLanguage(ctx *gin.Context) { + xlang := strings.ToLower(strings.TrimSpace(ctx.GetHeader("X-Lang"))) + + if !slices.Contains(language.ArrayString, xlang) { + xlang = string(language.PT) + } + + ctx.Set("lang", translations.Translations[xlang]) + + ctx.Next() +} diff --git a/pkg/utils/validator.go b/pkg/utils/validator.go index aeb9964..37c2337 100644 --- a/pkg/utils/validator.go +++ b/pkg/utils/validator.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/devkcud/arkhon-foundation/arkhon-api/pkg/language" + "github.com/devkcud/arkhon-foundation/arkhon-api/translations" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) @@ -17,7 +19,7 @@ type ParamError struct { } func (pe ParamError) Error() string { - return fmt.Sprintf("%s: %s", pe.Param, pe.Message) + return fmt.Sprintf("%s: %s", strings.ToLower(pe.Param), pe.Message) } func newValidator() *validator.Validate { @@ -92,55 +94,58 @@ func ValidateStruct(s any) validator.ValidationErrors { return nil } -func ValidateErrorMessage(fe validator.FieldError) ParamError { +func ValidateErrorMessage(ctx *gin.Context, fe validator.FieldError) ParamError { + dict := translations.GetTranslation(ctx) + field := strings.ToLower(fe.Field()) + if fe.Tag() == "min" { return ParamError{ - Param: fe.Field(), - Message: fmt.Sprintf("%s must have at least %s characters", fe.Field(), fe.Param()), + Param: field, + Message: fmt.Sprintf(dict.ValidatorMinChars, field, fe.Param()), } } if fe.Tag() == "max" { return ParamError{ - Param: fe.Field(), - Message: fmt.Sprintf("%s must have a maximum of %s characters", fe.Field(), fe.Param()), + Param: field, + Message: fmt.Sprintf(dict.ValidatorMaxChars, field, fe.Param()), } } switch fe.Tag() { case "required": return ParamError{ - Param: fe.Field(), - Message: fmt.Sprintf("%s is required", fe.Field()), + Param: field, + Message: fmt.Sprintf(dict.ValidatorRequired, field), } case "mustbesupportedlanguage": return ParamError{ - Param: fe.Field(), - Message: fmt.Sprintf("%s must be %s", fe.Field(), strings.Join(language.ArrayString, ", ")), + Param: field, + Message: fmt.Sprintf(dict.ValidatorMustBeSupportedLanguage, field, strings.Join(language.ArrayString, ", ")), } case "mustbenumericalboolean": return ParamError{ - Param: fe.Field(), - Message: "Value must be -1, 0 or 1", + Param: field, + Message: dict.ValidatorMustBeNumericalBoolean, } case "username": return ParamError{ - Param: fe.Field(), - Message: "Usernames must consist only of lowercase alphanumeric (a-z & 0-9) characters", + Param: field, + Message: dict.ValidatorIncorrectUsernameFormat, } case "email": return ParamError{ - Param: fe.Field(), - Message: "Email format is incorrect", + Param: field, + Message: dict.ValidatorIncorrectEmailFormat, } case "password": return ParamError{ - Param: fe.Field(), - Message: "The password should contain at least one uppercase letter, one lowercase letter, one special character, and one numeral", + Param: field, + Message: dict.ValidatorIncorrectPasswordFormat, } default: return ParamError{ - Param: fe.Field(), + Param: field, Message: fe.Error(), } } diff --git a/translations/configure.go b/translations/configure.go new file mode 100644 index 0000000..d98a84f --- /dev/null +++ b/translations/configure.go @@ -0,0 +1,95 @@ +package translations + +import ( + "log" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" +) + +type Translation struct { + Hello string `yaml:"hello"` + InvalidAPIKey string `yaml:"invalid_api_key"` + MaximumAPIKey string `yaml:"maximum_api_key"` + RequirePermissionAPIKey string `yaml:"require_permission_api_key"` + + InternalServerError string `yaml:"internal_server_error"` + InvalidBody string `yaml:"invalid_body"` + + NoAPIKeyFound string `yaml:"no_api_key_found"` // Used in queries for getting the permissions of keys + APIKeyDestroyed string `yaml:"api_key_destroyed"` + APIKeyUpdated string `yaml:"api_key_updated"` + + AuthDuplicatedUser string `yaml:"auth_duplicated_user"` + AuthUserDeleted string `yaml:"auth_user_deleted"` + AuthUserUpdated string `yaml:"auth_user_updated"` + AuthWrongCredentials string `yaml:"auth_wrong_credentials"` + + SearchIncorrect string `yaml:"search_incorrect"` + SearchNoResults string `yaml:"search_no_results"` + + UserDisabledFollowers string `yaml:"user_disabled_followers"` + UserDisabledFollowing string `yaml:"user_disabled_following"` + UserDisabledProfile string `yaml:"user_disabled_profile"` + UserErrorFollowItself string `yaml:"user_error_follow_itself"` + UserNotFound string `yaml:"user_not_found"` + UserFollowingAlready string `yaml:"user_following_already"` + UserFollowingNot string `yaml:"user_following_not"` + UserFollowingStarted string `yaml:"user_following_started"` + UserFollowingStopped string `yaml:"user_following_stopped"` + + ValidatorIncorrectEmailFormat string `yaml:"validator_incorrect_email_format"` + ValidatorIncorrectPasswordFormat string `yaml:"validator_incorrect_password_format"` + ValidatorIncorrectUsernameFormat string `yaml:"validator_incorrect_username_format"` + ValidatorMaxChars string `yaml:"validator_max_chars"` + ValidatorMinChars string `yaml:"validator_min_chars"` + ValidatorMustBeNumericalBoolean string `yaml:"validator_must_be_numerical_boolean"` + ValidatorMustBeSupportedLanguage string `yaml:"validator_must_be_supported_language"` + ValidatorRequired string `yaml:"validator_required"` +} + +var Translations = make(map[string]Translation) + +func readYAMLFile(filename string) (*Translation, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var translation Translation + err = yaml.Unmarshal(data, &translation) + if err != nil { + return nil, err + } + + return &translation, nil +} + +func Init(dir string) { + files, err := os.ReadDir(dir) + if err != nil { + log.Fatalf("error reading directory: %v", err) + } + + for _, file := range files { + if filepath.Ext(file.Name()) != ".yaml" { + continue + } + + lang := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())) + + translation, err := readYAMLFile(filepath.Join(dir, file.Name())) + if err != nil { + log.Fatalf("error reading %s: %v", file.Name(), err) + } + + Translations[lang] = *translation + } +} + +func GetTranslation(ctx *gin.Context) Translation { + return ctx.Keys["lang"].(Translation) +} diff --git a/translations/en.yaml b/translations/en.yaml new file mode 100644 index 0000000..1393421 --- /dev/null +++ b/translations/en.yaml @@ -0,0 +1,35 @@ +api_key_destroyed: API key was destroyed successfully. +api_key_updated: API key was updated successfully. +auth_duplicated_user: An user with that username or email already exists. +auth_user_deleted: User deleted. +auth_user_updated: User updated. +auth_wrong_credentials: Email, username or password are incorrect or don't exist. +hello: Hello! +internal_server_error: Internal server error. Please, try again later. +invalid_api_key: Invalid API key. +invalid_body: Invalid request body format. Please ensure the JSON structure is correct and all required + fields are present. +maximum_api_key: API key has reached its maximum allowed usage. Contact support or obtain a new key. +no_api_key_found: No API key found with that ID. +require_permission_api_key: 'This API key does not have permission to: %s' +search_incorrect: Searches cannot be composed of spaces only or special characters. +search_no_results: Nothing found with that search criteria. +user_disabled_followers: User disabled viewing their followers. +user_disabled_following: User disabled viewing whom are following them. +user_disabled_profile: User disabled viewing their profile. +user_error_follow_itself: Users cannot follow or unfollow themselves. +user_following_already: User is already following %s. +user_following_not: User is not following %s. +user_following_started: User started following %s. +user_following_stopped: User stopped following %s. +user_not_found: User not found; are you sure this is their username? +validator_incorrect_email_format: Email format is incorrect. +validator_incorrect_password_format: The password should have a minimum of 8 characters, contain at least + one uppercase letter, one lowercase letter, one special character, and one numeral. +validator_incorrect_username_format: Usernames must consist only of lowercase alphanumeric (a-z & 0-9) + characters. +validator_max_chars: '%s must have a maximum of %s characters.' +validator_min_chars: '%s must have at least %s characters.' +validator_must_be_numerical_boolean: Value must be -1, 0 or 1. +validator_must_be_supported_language: '%s must be one of the following supported languages: %s.' +validator_required: '%s is a required field.' diff --git a/translations/pt.yaml b/translations/pt.yaml new file mode 100644 index 0000000..11faa76 --- /dev/null +++ b/translations/pt.yaml @@ -0,0 +1,36 @@ +api_key_destroyed: Chave de API destruída com sucesso. +api_key_updated: Chave de API atualizada com sucesso. +auth_duplicated_user: Já existe um usuário com esse nome de usuário ou e-mail. +auth_user_deleted: Usuário deletado. +auth_user_updated: Usuário atualizado. +auth_wrong_credentials: Email, nome de usuário ou senha estão incorretos ou não existem. +hello: Olá! +internal_server_error: Erro interno de servidor. Por favor, tente novamente mais tarde. +invalid_api_key: Chave de API inválida. +invalid_body: Formato de corpo de solicitação inválido. Certifique-se de que a estrutura JSON esteja correta + e que todos os campos obrigatórios estejam presentes. +maximum_api_key: Chave de API chegou em seu limite máximo permitido. Contate o suporte ou obtenha uma + nova chave. +no_api_key_found: Nenhuma chave de API encontrada com este ID. +require_permission_api_key: 'Esta chave de API não tem a permissão de: %s' +search_incorrect: As pesquisas não podem ser compostas apenas por espaços ou caracteres especiais. +search_no_results: Nada encontrado com esse critério de pesquisa. +user_disabled_followers: Usuário desativou a visualização de seus seguidores. +user_disabled_following: Usuário desativou a visualização de quem o está seguindo. +user_disabled_profile: Usuário desativou a visualização de seu perfil. +user_error_follow_itself: Usuários não podem seguir ou deixar de seguir a si mesmos. +user_following_already: Usuário já seguindo %s. +user_following_not: Usuário ainda não está seguindo %s. +user_following_started: Usuário começou a seguir %s. +user_following_stopped: Usuário parou de seguir %s. +user_not_found: Usuário não encontrado; você tem certeza de que este é o nome de usuário? +validator_incorrect_email_format: Formato de email incorreto. +validator_incorrect_password_format: A senha deve ter no mínimo 8 caracteres, conter pelo menos uma letra + maiúscula, uma letra minúscula, um caractere especial e um numeral. +validator_incorrect_username_format: Os nomes de usuário devem consistir apenas de caracteres alfanuméricos + minúsculos (a-z e 0-9). +validator_max_chars: '%s deve ter no máximo %s caracteres.' +validator_min_chars: '%s deve ter no mínimo %s caracteres.' +validator_must_be_numerical_boolean: Valor deve ser -1, 0 ou 1. +validator_must_be_supported_language: '%s deve ser uma das seguintes linguas suportadas: %s.' +validator_required: '%s é um campo obrigatório.' diff --git a/translations/ru.yaml b/translations/ru.yaml new file mode 100644 index 0000000..7302259 --- /dev/null +++ b/translations/ru.yaml @@ -0,0 +1,36 @@ +api_key_destroyed: Ключ API успешно уничтожен. +api_key_updated: Ключ API успешно обновлен. +auth_duplicated_user: Уже существует пользователь с таким именем пользователя или электронной почтой. +auth_user_deleted: Пользователь удален. +auth_user_updated: Пользователь обновлен. +auth_wrong_credentials: Электронная почта, имя пользователя или пароль неверны или не существуют. +hello: Привет! +internal_server_error: Внутренняя ошибка сервера. Пожалуйста, попробуйте позже. +invalid_api_key: Неверный ключ API. +invalid_body: Недопустимый формат тела запроса. Пожалуйста, убедитесь, что структура JSON правильная и + все обязательные поля присутствуют. +maximum_api_key: Ключ API достиг максимального допустимого использования. Свяжитесь со службой поддержки + или получите новый ключ. +no_api_key_found: Ключ API с таким идентификатором не найден. +require_permission_api_key: 'Этот ключ API не имеет разрешения на: %s' +search_incorrect: Поиск не может состоять только из пробелов или специальных символов. +search_no_results: Ничего не найдено по заданным критериям поиска. +user_disabled_followers: Пользователь отключил просмотр своих подписчиков. +user_disabled_following: Пользователь отключил просмотр тех, кто следит за ним. +user_disabled_profile: Пользователь отключил просмотр своего профиля. +user_error_follow_itself: Пользователи не могут следовать или отстраняться от себя. +user_following_already: Пользователь уже следил за %s. +user_following_not: Пользователь не следует за %s. +user_following_started: Пользователь начал следовать %s. +user_following_stopped: Пользователь прекратил следовать за %s. +user_not_found: Пользователь не найден; вы уверены, что это его имя пользователя? +validator_incorrect_email_format: Формат электронной почты неверен. +validator_incorrect_password_format: Пароль должен состоять минимум из 8 символов, содержать как минимум + одну заглавную букву, одну строчную букву, один специальный символ и одну цифру. +validator_incorrect_username_format: Имена пользователей должны состоять только из строчных буквенно-цифровых + символов (a-z и 0-9). +validator_max_chars: '%s должно содержать не более %s символов.' +validator_min_chars: В %s должно быть не менее %s символов. +validator_must_be_numerical_boolean: Значение должно быть -1, 0 или 1. +validator_must_be_supported_language: '%s должен быть одним из следующих поддерживаемых языков: %s.' +validator_required: '%s - обязательное поле.'