From f7bee4604e2aae18fed2c7d201cadd11ffbb353e Mon Sep 17 00:00:00 2001 From: Joan Manuel Jaramillo Avila <89425013+LifeRIP@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:04:27 -0500 Subject: [PATCH 1/2] feat: Adds user existence validation and Firebase integration - Introduces functionality to ensure a user's existence in the database, leveraging Firebase authentication data. - Updates `UserHandler` and related services to support this logic. - Adds a new route `/user/ensure` for user validation and creation. - Enhances `UserRepository` and `UserService` with methods to fetch and create users. --- docs/docs.go | 38 +++++++++++++++++++++ docs/swagger.json | 38 +++++++++++++++++++++ docs/swagger.yaml | 25 ++++++++++++++ internal/handlers/chat_handler.go | 38 ++++++++++----------- internal/handlers/user_handler.go | 43 +++++++++++++++++++++++- internal/repositories/user_repository.go | 20 +++++++++++ internal/routes/router.go | 9 +++++ internal/services/user_service.go | 28 +++++++++++++++ 8 files changed, 219 insertions(+), 20 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index ed790b8..7676e8a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -756,6 +756,40 @@ const docTemplate = `{ } } } + }, + "/user/ensure": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Verifica si el usuario autenticado existe en la base de datos, si no, lo crea con los datos de autenticación", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Asegura que el usuario exista en la base de datos", + "responses": { + "200": { + "description": "Datos del usuario", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "500": { + "description": "Error interno del servidor", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -829,6 +863,10 @@ const docTemplate = `{ "createdAt": { "type": "string" }, + "displayName": { + "description": "Excluido de Firestore", + "type": "string" + }, "id": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 7e57785..7fcfd37 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -749,6 +749,40 @@ } } } + }, + "/user/ensure": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Verifica si el usuario autenticado existe en la base de datos, si no, lo crea con los datos de autenticación", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Asegura que el usuario exista en la base de datos", + "responses": { + "200": { + "description": "Datos del usuario", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "500": { + "description": "Error interno del servidor", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -822,6 +856,10 @@ "createdAt": { "type": "string" }, + "displayName": { + "description": "Excluido de Firestore", + "type": "string" + }, "id": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ecf85a5..107c9af 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -46,6 +46,9 @@ definitions: type: string createdAt: type: string + displayName: + description: Excluido de Firestore + type: string id: type: string isDeleted: @@ -626,6 +629,28 @@ paths: summary: Conexión WebSocket para chat en tiempo real tags: - Chat + /user/ensure: + post: + consumes: + - application/json + description: Verifica si el usuario autenticado existe en la base de datos, + si no, lo crea con los datos de autenticación + produces: + - application/json + responses: + "200": + description: Datos del usuario + schema: + $ref: '#/definitions/models.User' + "500": + description: Error interno del servidor + schema: + type: string + security: + - BearerAuth: [] + summary: Asegura que el usuario exista en la base de datos + tags: + - User securityDefinitions: BearerAuth: in: header diff --git a/internal/handlers/chat_handler.go b/internal/handlers/chat_handler.go index 24dd743..59b138b 100644 --- a/internal/handlers/chat_handler.go +++ b/internal/handlers/chat_handler.go @@ -127,13 +127,13 @@ func (h *ChatHandler) GetUserRooms(w http.ResponseWriter, r *http.Request) { // @Accept json // @Produce json // @Security BearerAuth -// @Param roomId path string true "ID de la sala" -// @Param limit query int false "Límite de mensajes a obtener" default(50) -// @Param cursor query string false "Cursor para paginación (timestamp)" default("1747441934") +// @Param roomId path string true "ID de la sala" +// @Param limit query int false "Límite de mensajes a obtener" default(50) +// @Param cursor query string false "Cursor para paginación (timestamp)" default("1747441934") // @Success 200 {object} models.PaginatedMessagesResponse "Mensajes paginados de la sala" -// @Failure 401 {string} string "No autorizado" -// @Failure 404 {string} string "Sala no encontrada" -// @Failure 500 {string} string "Error interno del servidor" +// @Failure 401 {string} string "No autorizado" +// @Failure 404 {string} string "Sala no encontrada" +// @Failure 500 {string} string "Error interno del servidor" // @Router /chat/rooms/{roomId}/messages/paginated [get] func (h *ChatHandler) GetRoomMessages(w http.ResponseWriter, r *http.Request) { roomID := chi.URLParam(r, "roomId") @@ -342,19 +342,19 @@ func (h *ChatHandler) JoinRoom(w http.ResponseWriter, r *http.Request) { // GetRoomMessagesSimple obtiene los mensajes de una sala sin paginación // -// @Summary Obtiene mensajes de una sala (versión simple) -// @Description Devuelve los mensajes de una sala específica sin paginación -// @Tags Chat -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param roomId path string true "ID de la sala" -// @Param limit query int false "Límite de mensajes a obtener" default(50) -// @Success 200 {array} models.MessageResponse "Lista de mensajes de la sala" -// @Failure 401 {string} string "No autorizado" -// @Failure 404 {string} string "Sala no encontrada" -// @Failure 500 {string} string "Error interno del servidor" -// @Router /chat/rooms/{roomId}/messages [get] +// @Summary Obtiene mensajes de una sala (versión simple) +// @Description Devuelve los mensajes de una sala específica sin paginación +// @Tags Chat +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param roomId path string true "ID de la sala" +// @Param limit query int false "Límite de mensajes a obtener" default(50) +// @Success 200 {array} models.MessageResponse "Lista de mensajes de la sala" +// @Failure 401 {string} string "No autorizado" +// @Failure 404 {string} string "Sala no encontrada" +// @Failure 500 {string} string "Error interno del servidor" +// @Router /chat/rooms/{roomId}/messages [get] func (h *ChatHandler) GetRoomMessagesSimple(w http.ResponseWriter, r *http.Request) { roomID := chi.URLParam(r, "roomId") diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go index a2a1cd4..70a8454 100644 --- a/internal/handlers/user_handler.go +++ b/internal/handlers/user_handler.go @@ -10,12 +10,14 @@ import ( type UserHandler struct { UserService *services.UserService + AuthService *services.AuthService } // NewUserHandler crea una nueva instancia de UserHandler -func NewUserHandler(userService *services.UserService) *UserHandler { +func NewUserHandler(userService *services.UserService, authService *services.AuthService) *UserHandler { return &UserHandler{ UserService: userService, + AuthService: authService, } } @@ -35,3 +37,42 @@ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } + +// EnsureUserExists verifica si el usuario existe en la base de datos, si no, lo crea con los datos de autenticación +// +// @Summary Asegura que el usuario exista en la base de datos +// @Description Verifica si el usuario autenticado existe en la base de datos, si no, lo crea con los datos de autenticación +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.User "Datos del usuario" +// @Failure 500 {string} string "Error interno del servidor" +// @Router /user/ensure [post] +func (h *UserHandler) EnsureUserExists(w http.ResponseWriter, r *http.Request) { + // Obtener el ID del usuario del contexto + userID, ok := r.Context().Value("userID").(string) + if !ok { + http.Error(w, "User ID not found in context", http.StatusInternalServerError) + return + } + + // Obtener los datos del usuario desde Firebase Auth + authUser, err := h.AuthService.GetUserByID(r.Context(), userID) + if err != nil { + http.Error(w, "Error getting user from auth: "+err.Error(), http.StatusInternalServerError) + return + } + println("Auth User:", authUser.UID, authUser.Email, authUser.DisplayName) + + // Asegurar que el usuario exista en la base de datos + user, err := h.UserService.EnsureUserExists(r.Context(), authUser) + if err != nil { + http.Error(w, "Error ensuring user exists: "+err.Error(), http.StatusInternalServerError) + return + } + + // Responder con los datos del usuario + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index 1de3a3f..2e238f6 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -35,3 +35,23 @@ func (r *UserRepository) CreateUser(user *models.User) error { return nil } + +// GetUserByID obtiene un usuario de la base de datos por su ID +func (r *UserRepository) GetUserByID(ctx context.Context, userID string) (*models.User, error) { + docRef := r.FirestoreClient.Client.Collection("users").Doc(userID) + docSnap, err := docRef.Get(ctx) + if err != nil { + return nil, err + } + + if !docSnap.Exists() { + return nil, nil // El usuario no existe + } + + var user models.User + if err := docSnap.DataTo(&user); err != nil { + return nil, err + } + + return &user, nil +} diff --git a/internal/routes/router.go b/internal/routes/router.go index 90021b0..178e23d 100644 --- a/internal/routes/router.go +++ b/internal/routes/router.go @@ -14,6 +14,7 @@ import ( // NewRouter crea un nuevo router HTTP func NewRouter( authHandler *handlers.AuthHandler, + userHandler *handlers.UserHandler, // Add userHandler parameter chatHandler *handlers.ChatHandler, webSocketHandler *handlers.WebSocketHandler, authMw *authMiddleware.AuthMiddleware, @@ -45,6 +46,14 @@ func NewRouter( // Rutas protegidas (requieren token) r.Route("/api/v1", func(r chi.Router) { // Rutas de usuario + r.Route("/user", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(authMw.VerifyToken) // Aplicar middleware de autenticación + r.Post("/create", userHandler.EnsureUserExists) // Nueva ruta para asegurar que el usuario exista + }) + }) + + // Rutas de autenticación r.Route("/auth", func(r chi.Router) { r.Post("/signup", authHandler.SignUpAndCreateUser) // Ruta para registrar y crear un nuevo usuario diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 631bbc7..e3c56a7 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -1,6 +1,8 @@ package services import ( + "context" + "github.com/Parchat/backend/internal/config" "github.com/Parchat/backend/internal/models" "github.com/Parchat/backend/internal/repositories" @@ -28,3 +30,29 @@ func (s *UserService) CreateUser(user *models.User) error { return nil } + +// GetUserByID obtiene un usuario de la base de datos por su ID +func (s *UserService) GetUserByID(ctx context.Context, userID string) (*models.User, error) { + return s.UserRepo.GetUserByID(ctx, userID) +} + +// EnsureUserExists verifica si el usuario existe en la base de datos, si no, lo crea +func (s *UserService) EnsureUserExists(ctx context.Context, authUser *models.User) (*models.User, error) { + // Verificar si el usuario ya existe en la base de datos + user, err := s.GetUserByID(ctx, authUser.UID) + + // Si hay un error pero NO es del tipo "no encontrado", retornamos el error + // Si es un error de "no encontrado" o si no hay error pero user es nil, creamos el usuario + if err == nil && user != nil { + // Usuario encontrado, lo retornamos + return user, nil + } + + // Si llegamos aquí, o hubo un error de "usuario no encontrado" o user es nil, + // en ambos casos queremos crear un nuevo usuario + err = s.CreateUser(authUser) + if err != nil { + return nil, err + } + return authUser, nil +} From 51a2425d2c2c072e6167a02e03301deaca358da8 Mon Sep 17 00:00:00 2001 From: Joan Manuel Jaramillo Avila <89425013+LifeRIP@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:08:54 -0500 Subject: [PATCH 2/2] feat: Rename user endpoint from /user/ensure to /user/create in docs --- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- internal/handlers/user_handler.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 7676e8a..06bcd0c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -757,7 +757,7 @@ const docTemplate = `{ } } }, - "/user/ensure": { + "/user/create": { "post": { "security": [ { diff --git a/docs/swagger.json b/docs/swagger.json index 7fcfd37..f1a79d3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -750,7 +750,7 @@ } } }, - "/user/ensure": { + "/user/create": { "post": { "security": [ { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 107c9af..24948a1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -629,7 +629,7 @@ paths: summary: Conexión WebSocket para chat en tiempo real tags: - Chat - /user/ensure: + /user/create: post: consumes: - application/json diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go index 70a8454..10151e2 100644 --- a/internal/handlers/user_handler.go +++ b/internal/handlers/user_handler.go @@ -48,7 +48,7 @@ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { // @Security BearerAuth // @Success 200 {object} models.User "Datos del usuario" // @Failure 500 {string} string "Error interno del servidor" -// @Router /user/ensure [post] +// @Router /user/create [post] func (h *UserHandler) EnsureUserExists(w http.ResponseWriter, r *http.Request) { // Obtener el ID del usuario del contexto userID, ok := r.Context().Value("userID").(string)