From 723da881ec8411935d4c06f96d7729328b451e48 Mon Sep 17 00:00:00 2001 From: maxogod Date: Wed, 11 Jun 2025 22:27:56 -0300 Subject: [PATCH 1/2] docs: New README documentation --- README.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index abf568f..33439ab 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,34 @@ Este microservicio tiene como responsabilidad el manejo de autenticacion de usuarios, ya sea creacion, login, validacion de email y demas datos, el manejo de sesiones utilizando stateless sessions con jwt tokens necesarias para el resto -de los microservicios y el manejo de los perfiles incluyendo ediciones, almacenamiento de imagenes y proteccion de datos privados. +de los microservicios, el manejo de los perfiles incluyendo ediciones, almacenamiento de imagenes y proteccion de datos privados +y finalmente la busqueda y administracion de usuarios. ## Endpoints Los endpoints de este microservicio se pueden encontrar en el *swagger* del mismo (o alternativamente en la carpeta `docs/`), los mismos son: +* `GET` /swagger/index.html (Documentacion de la API) + * `GET` **/auth** (Validar sesion actual) * `GET` **/auth/activate/:id?token=string&refresh=bool** (Activacion de cuenta) +* `POST` **/auth/recovery?token=string&refresh=bool** (Recupero de cuenta) + * `POST` **/auth/google** (Inicio de sesion/Creacion de cuenta con Google) * `POST` **/auth/login** (Inicio de sesion con email y password) * `POST` **/signup** (Creacion de cuenta con email y password) + * `GET` **/profile** (Obtener perfil de la sesion actual) * `PATCH` **/profile** (Editar perfil de la sesion actual) * `PUT` **/profile/picture** (Subir imagen como foto de perfil) * `DELETE` **/profile/picture** (Borrar foto de perfil) * `GET` **/profile/:id** (Obtener perfil de un usuario dado) +* `GET` **/management?name_contains=string&page=int** (Buscar usuarios por nombre, con paginacion) +* `PUT` **/management/block_status/:id** (Bloquear/Desbloquear usuario) - *admin only* +* `DELETE` **/management/:id** (Eliminar usuario) - *admin only* + ## Estructura Se utiliza la arquitectura package by layer, donde los controladores se pueden encontrar en la @@ -33,22 +43,32 @@ carpeta *handlers*, los servicios en *services* y los repositorios en *repositor Para cada grupo de rutas hay una interfaz para el Handler y otra para el Service, sin embargo solamente hay dos interfaces diferentes para repositorios (una de users repository y otra de profiles repository). -## Desarrollo local +## Desarrollo ### Docker-compose de uso local +Para correr el microservicio en un entorno de desarrollo local (con base de datos local incluida), +se puede utilizar el siguiente comando: + ```bash docker compose -f local-dev-compose.yml up --build ``` ### Migraciones +Para crear una migracion para cambiar tablas/entradas de la base de datos, se utiliza el comando go-migrate de la siguiente manera: + ```bash -migrate create -ext sql -dir migrations -seq create_assignments_table +# Instalar go-migrate si no se tiene instalado +go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + +# Crear una migracion nueva (alternativamente, crear archivos manualmente con el nombre deseado) +migrate create -ext sql -dir ./migrations -seq migration_name ``` ## Despliegue -### Pipelines - -### Como desplegar a produccion +1. Se hace feature branching desde la rama **dev**. +2. Una vez listo para integrar se realiza PR a **dev**, donde corre el pipeline de testing y coverage. +3. Una vez esta listo para desplegar se realiza PR a **main**, se vuelven a correr pipelines de calidad de codigo. +4. Cuando se hace el push a **main** se ejecuta el pipeline de continous delivery, construye la imagen y la desplega a k8s. From 18fd2f92dee0779d6200782ca587495f18187485 Mon Sep 17 00:00:00 2001 From: maxogod Date: Tue, 17 Jun 2025 02:50:48 -0300 Subject: [PATCH 2/2] feat: Add education level to JWT --- internal/handlers/users/auth/login.go | 48 +++++++++++++++------- internal/handlers/users/auth/login_test.go | 28 ++++++++++--- internal/repository/memory/profiles.go | 5 ++- internal/router/auth_router.go | 6 ++- internal/utils/tokens.go | 14 +++++-- internal/utils/tokens_test.go | 2 +- 6 files changed, 76 insertions(+), 27 deletions(-) diff --git a/internal/handlers/users/auth/login.go b/internal/handlers/users/auth/login.go index 67e36c0..f3a1350 100644 --- a/internal/handlers/users/auth/login.go +++ b/internal/handlers/users/auth/login.go @@ -9,6 +9,7 @@ import ( "users-microservice/internal/logging" "users-microservice/internal/models" "users-microservice/internal/services/users/auth" + "users-microservice/internal/services/users/profile" "users-microservice/internal/utils" "github.com/gin-gonic/gin" @@ -16,14 +17,16 @@ import ( ) type authHandler struct { - service auth.AuthService - conf *config.Config + userService auth.AuthService + profileService profile.ProfileService + conf *config.Config } -func NewAuthHandler(service auth.AuthService, conf *config.Config) AuthHandler { +func NewAuthHandler(userService auth.AuthService, profileService profile.ProfileService, conf *config.Config) AuthHandler { return &authHandler{ - service: service, - conf: conf, + userService: userService, + profileService: profileService, + conf: conf, } } @@ -53,14 +56,19 @@ func (h *authHandler) Login(c *gin.Context) { return } - user, err := h.service.Login(loginInfo.Email, loginInfo.Password) + user, err := h.userService.Login(loginInfo.Email, loginInfo.Password) if err != nil { _ = c.Error(err) return } if user.Active { - token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, h.conf.JWT_SECRET) + profile, err := h.profileService.GetProfile(user.ID, user.ID) + if err != nil { + _ = c.Error(errors.NewInternalServerError("Failed to retrieve user profile: " + err.Error())) + return + } + token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, profile.Education, h.conf.JWT_SECRET) if err != nil { logging.GetLogger().Errorln("Failed to create JWT token: ", err) } else { @@ -106,14 +114,19 @@ func (h *authHandler) GoogleLogin(c *gin.Context) { confirmAccountMerge := c.Query("confirm_account_merge") == "true" created := false - user, err := h.service.GoogleLogin(googleLoginInfo.IdToken, googleLoginInfo.Type, &created, confirmAccountMerge) + user, err := h.userService.GoogleLogin(googleLoginInfo.IdToken, googleLoginInfo.Type, &created, confirmAccountMerge) if err != nil { _ = c.Error(err) return } if user.Active { - token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, h.conf.JWT_SECRET) + profile, err := h.profileService.GetProfile(user.ID, user.ID) + if err != nil { + _ = c.Error(errors.NewInternalServerError("Failed to retrieve user profile: " + err.Error())) + return + } + token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, profile.Education, h.conf.JWT_SECRET) if err != nil { logging.GetLogger().Errorln("Failed to create JWT token: ", err) } else { @@ -187,13 +200,13 @@ func (h *authHandler) AccountActivation(c *gin.Context) { var user *models.User if token != "" { - user, err = h.service.ActivateAccount(parsedID, token) + user, err = h.userService.ActivateAccount(parsedID, token) if err != nil { _ = c.Error(err) return } } else if refresh == "true" { - user, err = h.service.RefreshActivationToken(parsedID) + user, err = h.userService.RefreshActivationToken(parsedID) if err != nil { _ = c.Error(err) return @@ -201,7 +214,12 @@ func (h *authHandler) AccountActivation(c *gin.Context) { } if user.Active { - token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, h.conf.JWT_SECRET) + profile, err := h.profileService.GetProfile(user.ID, user.ID) + if err != nil { + _ = c.Error(errors.NewInternalServerError("Failed to retrieve user profile: " + err.Error())) + return + } + token, err := utils.GenerateJWTAuthorizationToken(user.ID.String(), user.Email, profile.Education, h.conf.JWT_SECRET) if err != nil { logging.GetLogger().Errorln("Failed to create JWT token: ", err) } else { @@ -248,13 +266,13 @@ func (h *authHandler) RecoverPassword(c *gin.Context) { _ = c.Error(errors.NewBadRequestError("password must be at least 8 characters long")) return } - err := h.service.ChangePassword(recoveryInfo.Email, recoveryInfo.Password, token) + err := h.userService.ChangePassword(recoveryInfo.Email, recoveryInfo.Password, token) if err != nil { _ = c.Error(err) return } } else if refresh == "true" { - err := h.service.RefreshPasswordRecoveryCode(recoveryInfo.Email) + err := h.userService.RefreshPasswordRecoveryCode(recoveryInfo.Email) if err != nil { _ = c.Error(err) return @@ -275,7 +293,7 @@ func (h *authHandler) handleValidSession(c *gin.Context) error { if err != nil { return errors.NewUnauthorizedError("invalid token: " + err.Error()) } - user, err := h.service.VerifySession(parsedID, c.GetBool("invalidJWT"), c.GetBool("expiredJWT")) + user, err := h.userService.VerifySession(parsedID, c.GetBool("invalidJWT"), c.GetBool("expiredJWT")) if err != nil { return err } diff --git a/internal/handlers/users/auth/login_test.go b/internal/handlers/users/auth/login_test.go index a8d3b3b..c42ba2f 100644 --- a/internal/handlers/users/auth/login_test.go +++ b/internal/handlers/users/auth/login_test.go @@ -15,6 +15,7 @@ import ( "users-microservice/internal/repository/memory" "users-microservice/internal/repository/users" auth_service "users-microservice/internal/services/users/auth" + "users-microservice/internal/services/users/profile" "users-microservice/internal/utils" "github.com/gin-gonic/gin" @@ -35,24 +36,41 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - _ = repo.CreateUser(&models.User{ + user1 := &models.User{ Name: "Test User", Email: "test@email.com", Password: string(hashedPassword), - }) - _ = repo.CreateUser(&models.User{ + } + user2 := &models.User{ Name: "Test User 2", Email: "test2@email.com", Password: string(hashedPassword), Active: true, // This user is already activated - }) + } + _ = repo.CreateUser(user1) + _ = repo.CreateUser(user2) + + profileRepo := memory.NewInMemoryProfilesRepository( + []models.Profile{ + { + UserID: user1.ID, + Education: models.HighSchool, + }, + { + UserID: user2.ID, + Education: models.HighSchool, + }, + }, + ) conf := &config.Config{ GOOGLE_CLIENT_ID: "test-client-id", + JWT_SECRET: []byte("test-secret"), } utils.VerifyGoogleToken = mockVerifyGoogleLogin // override func service := auth_service.NewAuthService(repo, conf) - authHandler = auth_handler.NewAuthHandler(service, conf) + profileService := profile.NewProfileService(profileRepo, repo, conf) + authHandler = auth_handler.NewAuthHandler(service, profileService, conf) // Mock router gin.SetMode(gin.TestMode) diff --git a/internal/repository/memory/profiles.go b/internal/repository/memory/profiles.go index 3892553..2facb63 100644 --- a/internal/repository/memory/profiles.go +++ b/internal/repository/memory/profiles.go @@ -55,7 +55,10 @@ func (r *inMemoryProfilesRepository) DeletePictureById(id uuid.UUID) error { func (r *inMemoryProfilesRepository) GetProfileByID(id uuid.UUID) (*models.Profile, error) { profile, exists := r.profilesDB[id] if !exists { - return nil, errors.NewNotFoundError("profile not found") + return &models.Profile{ + UserID: id, + Education: models.NoEducation, + }, nil } return &profile, nil diff --git a/internal/router/auth_router.go b/internal/router/auth_router.go index cca0af3..e179194 100644 --- a/internal/router/auth_router.go +++ b/internal/router/auth_router.go @@ -3,8 +3,10 @@ package router import ( "users-microservice/config" auth_handler "users-microservice/internal/handlers/users/auth" + profile_repository "users-microservice/internal/repository/profiles" users_repository "users-microservice/internal/repository/users" auth_service "users-microservice/internal/services/users/auth" + profile_service "users-microservice/internal/services/users/profile" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -14,8 +16,10 @@ import ( // the corresponding handlers for the auth routes. func InitAuthRoutes(r *gin.Engine, db *gorm.DB, conf *config.Config) { usersRepository := users_repository.NewUsersRepository(db, conf) + profileRepository := profile_repository.NewProfilesRepository(db, conf) authService := auth_service.NewAuthService(usersRepository, conf) - authHandler := auth_handler.NewAuthHandler(authService, conf) + profileService := profile_service.NewProfileService(profileRepository, usersRepository, conf) + authHandler := auth_handler.NewAuthHandler(authService, profileService, conf) group := r.Group("/auth") diff --git a/internal/utils/tokens.go b/internal/utils/tokens.go index 44c3560..778cd67 100644 --- a/internal/utils/tokens.go +++ b/internal/utils/tokens.go @@ -3,6 +3,7 @@ package utils import ( "crypto/rand" "math/big" + "strconv" "time" "github.com/golang-jwt/jwt/v5" @@ -11,11 +12,16 @@ import ( const activationTokenLength = 8 const activationTokenCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -func GenerateJWTAuthorizationToken(id string, email string, secret []byte) (string, error) { +func GenerateJWTAuthorizationToken(id string, email string, educationLevel *int, secret []byte) (string, error) { + education := 0 + if educationLevel != nil { + education = *educationLevel + } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "userID": id, - "email": email, - "exp": time.Now().Add(24 * time.Hour).Unix(), + "userID": id, + "email": email, + "educationLevel": strconv.Itoa(education), + "exp": time.Now().Add(24 * time.Hour).Unix(), }) tokenString, err := token.SignedString(secret) diff --git a/internal/utils/tokens_test.go b/internal/utils/tokens_test.go index f6ce280..05ab7ea 100644 --- a/internal/utils/tokens_test.go +++ b/internal/utils/tokens_test.go @@ -13,7 +13,7 @@ func TestGenerateJWTAuthorizationToken(t *testing.T) { id := "12345" email := "test@email.com" secret := []byte("secret") - token, err := utils.GenerateJWTAuthorizationToken(id, email, secret) + token, err := utils.GenerateJWTAuthorizationToken(id, email, nil, secret) assert.NoError(t, err) assert.NotEmpty(t, token)