Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
48 changes: 33 additions & 15 deletions internal/handlers/users/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@
"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"
"github.com/google/uuid"
)

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,
}
}

Expand Down Expand Up @@ -53,14 +56,19 @@
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)

Check warning on line 71 in internal/handlers/users/auth/login.go

View check run for this annotation

Codecov / codecov/patch

internal/handlers/users/auth/login.go#L66-L71

Added lines #L66 - L71 were not covered by tests
if err != nil {
logging.GetLogger().Errorln("Failed to create JWT token: ", err)
} else {
Expand Down Expand Up @@ -106,14 +114,19 @@
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
}

Check warning on line 128 in internal/handlers/users/auth/login.go

View check run for this annotation

Codecov / codecov/patch

internal/handlers/users/auth/login.go#L126-L128

Added lines #L126 - L128 were not covered by tests
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 {
Expand Down Expand Up @@ -187,21 +200,26 @@

var user *models.User
if token != "" {
user, err = h.service.ActivateAccount(parsedID, token)
user, err = h.userService.ActivateAccount(parsedID, token)

Check warning on line 203 in internal/handlers/users/auth/login.go

View check run for this annotation

Codecov / codecov/patch

internal/handlers/users/auth/login.go#L203

Added line #L203 was not covered by tests
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
}
}

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)

Check warning on line 222 in internal/handlers/users/auth/login.go

View check run for this annotation

Codecov / codecov/patch

internal/handlers/users/auth/login.go#L217-L222

Added lines #L217 - L222 were not covered by tests
if err != nil {
logging.GetLogger().Errorln("Failed to create JWT token: ", err)
} else {
Expand Down Expand Up @@ -248,13 +266,13 @@
_ = 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
Expand All @@ -275,7 +293,7 @@
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
}
Expand Down
28 changes: 23 additions & 5 deletions internal/handlers/users/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion internal/repository/memory/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion internal/router/auth_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")

Expand Down
14 changes: 10 additions & 4 deletions internal/utils/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"crypto/rand"
"math/big"
"strconv"
"time"

"github.com/golang-jwt/jwt/v5"
Expand All @@ -11,11 +12,16 @@
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
}

Check warning on line 19 in internal/utils/tokens.go

View check run for this annotation

Codecov / codecov/patch

internal/utils/tokens.go#L18-L19

Added lines #L18 - L19 were not covered by tests
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)
Expand Down
2 changes: 1 addition & 1 deletion internal/utils/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down