From 78c284c28a8d7245ef7f818f7f000dd28d219c06 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 15 Dec 2025 11:17:34 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Implementado=20a=20API=20de=20backend?= =?UTF-8?q?=20inicial=20e=20a=20interface=20de=20usu=C3=A1rio=20de=20front?= =?UTF-8?q?end=20para=20autentica=C3=A7=C3=A3o,=20gerenciamento=20de=20usu?= =?UTF-8?q?=C3=A1rios=20e=20cadastro=20profissional.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit implementação de listagem de usuários admin, padronização de roles e melhorias nos cadastros Backend: - Adicionados endpoints administrativos [ListUsers](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:268:0-270:1) e [GetUser](cci:1://file:///c:/Projetos/photum/backend/internal/auth/handler.go:475:0-514:1). - Padronizadas as constantes de [UserRole](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:202:0-216:1) (`SUPERADMIN`, `BUSINESS_OWNER`, etc.) para alinhar com o frontend. - Atualizada a função [EnsureDemoUsers](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:230:0-266:1) para migrar usuários existentes para as novas roles. - Documentação Swagger regenerada. Frontend: - Adicionado busca automática de CEP no formulário de Cadastro Profissional (AwesomeAPI). - Adicionado texto de ajuda e ordenação (priorizando "Não Cadastrado") no select de Empresas. --- backend/cmd/api/main.go | 33 +- backend/docs/docs.go | 415 +++++++++++++++++- backend/docs/swagger.json | 415 +++++++++++++++++- backend/docs/swagger.yaml | 261 +++++++++++ backend/internal/auth/handler.go | 262 ++++++++++- backend/internal/auth/service.go | 168 ++++++- backend/internal/db/generated/usuarios.sql.go | 143 ++++++ backend/internal/db/queries/usuarios.sql | 24 + frontend/components/Navbar.tsx | 230 +++++----- frontend/components/ProfessionalForm.tsx | 48 ++ frontend/contexts/AuthContext.tsx | 35 +- frontend/contexts/DataContext.tsx | 79 +++- frontend/pages/Login.tsx | 6 +- frontend/pages/ProfessionalRegister.tsx | 29 +- frontend/pages/Register.tsx | 41 +- frontend/services/apiService.ts | 68 +++ 16 files changed, 2059 insertions(+), 198 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 11ba763..fdc4443 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "log" "photum-backend/docs" @@ -22,8 +23,7 @@ import ( "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" - - _ "photum-backend/docs" // Import generated docs + // "photum-backend/docs" is already imported above ) // @title Photum Backend API @@ -40,6 +40,10 @@ import ( // @host localhost:8080 // @BasePath / +// @tag.name auth +// @tag.description Authentication related operations +// @tag.name admin +// @tag.description Administration operations // @securityDefinitions.apikey BearerAuth // @in header @@ -65,6 +69,11 @@ func main() { tiposEventosService := tipos_eventos.NewService(queries) cadastroFotService := cadastro_fot.NewService(queries) + // Seed Demo Users + if err := authService.EnsureDemoUsers(context.Background()); err != nil { + log.Printf("Failed to seed demo users: %v", err) + } + // Initialize handlers authHandler := auth.NewHandler(authService) profissionaisHandler := profissionais.NewHandler(profissionaisService) @@ -88,7 +97,6 @@ func main() { configCors.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} r.Use(cors.New(configCors)) - // Swagger // Swagger // Dynamically update Swagger Info docs.SwaggerInfo.Host = cfg.SwaggerHost @@ -98,7 +106,13 @@ func main() { docs.SwaggerInfo.Schemes = []string{"http", "https"} } - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + // Swagger UI + url := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, + ginSwagger.PersistAuthorization(true), + ginSwagger.DeepLinking(true), + url, + )) // Public Routes authGroup := r.Group("/auth") @@ -175,6 +189,17 @@ func main() { api.GET("/cadastro-fot/:id", cadastroFotHandler.Get) api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update) api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete) + + admin := api.Group("/admin") + { + admin.GET("/users", authHandler.ListUsers) + admin.GET("/users/pending", authHandler.ListPending) + admin.GET("/users/:id", authHandler.GetUser) + admin.PATCH("/users/:id/approve", authHandler.Approve) + admin.POST("/users", authHandler.AdminCreateUser) + admin.PATCH("/users/:id/role", authHandler.UpdateRole) + admin.DELETE("/users/:id", authHandler.DeleteUser) + } } log.Printf("Swagger Host Configured: %s", cfg.SwaggerHost) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index d5e9cd7..4ea556f 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -24,6 +24,392 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/admin/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all users (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new user with specific role (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create user (Admin)", + "parameters": [ + { + "description": "Create User Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.registerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/pending": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List users with ativo=false", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List pending users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get user details by ID (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get user by ID", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete user (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}/approve": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Set user ativo=true", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Approve user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update user role (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update user role", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Role Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.updateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/anos-formaturas": { "get": { "security": [ @@ -1786,11 +2172,13 @@ const docTemplate = `{ ], "properties": { "email": { - "type": "string" + "type": "string", + "example": "admin@photum.com" }, "senha": { "type": "string", - "minLength": 6 + "minLength": 6, + "example": "123456" } } }, @@ -1837,6 +2225,17 @@ const docTemplate = `{ } } }, + "auth.updateRoleRequest": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "type": "string" + } + } + }, "auth.userResponse": { "type": "object", "properties": { @@ -2325,7 +2724,17 @@ const docTemplate = `{ "name": "Authorization", "in": "header" } - } + }, + "tags": [ + { + "description": "Authentication related operations", + "name": "auth" + }, + { + "description": "Administration operations", + "name": "admin" + } + ] }` // SwaggerInfo holds exported Swagger Info so clients can modify it diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 5b7f506..2a3c5e8 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -18,6 +18,392 @@ "host": "localhost:8080", "basePath": "/", "paths": { + "/api/admin/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all users (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new user with specific role (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create user (Admin)", + "parameters": [ + { + "description": "Create User Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.registerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/pending": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List users with ativo=false", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List pending users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get user details by ID (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get user by ID", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete user (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}/approve": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Set user ativo=true", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Approve user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/admin/users/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update user role (Admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update user role", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Role Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.updateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/anos-formaturas": { "get": { "security": [ @@ -1780,11 +2166,13 @@ ], "properties": { "email": { - "type": "string" + "type": "string", + "example": "admin@photum.com" }, "senha": { "type": "string", - "minLength": 6 + "minLength": 6, + "example": "123456" } } }, @@ -1831,6 +2219,17 @@ } } }, + "auth.updateRoleRequest": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "type": "string" + } + } + }, "auth.userResponse": { "type": "object", "properties": { @@ -2319,5 +2718,15 @@ "name": "Authorization", "in": "header" } - } + }, + "tags": [ + { + "description": "Authentication related operations", + "name": "auth" + }, + { + "description": "Administration operations", + "name": "admin" + } + ] } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 4c4135e..a3f6fda 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -18,8 +18,10 @@ definitions: auth.loginRequest: properties: email: + example: admin@photum.com type: string senha: + example: "123456" minLength: 6 type: string required: @@ -56,6 +58,13 @@ definitions: - role - senha type: object + auth.updateRoleRequest: + properties: + role: + type: string + required: + - role + type: object auth.userResponse: properties: ativo: @@ -385,6 +394,253 @@ info: title: Photum Backend API version: "1.0" paths: + /api/admin/users: + get: + consumes: + - application/json + description: List all users (Admin only) + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + additionalProperties: true + type: object + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: List all users + tags: + - admin + post: + consumes: + - application/json + description: Create a new user with specific role (Admin only) + parameters: + - description: Create User Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/auth.registerRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Create user (Admin) + tags: + - admin + /api/admin/users/{id}: + delete: + consumes: + - application/json + description: Delete user (Admin only) + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Delete user + tags: + - admin + get: + consumes: + - application/json + description: Get user details by ID (Admin only) + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Get user by ID + tags: + - admin + /api/admin/users/{id}/approve: + patch: + consumes: + - application/json + description: Set user ativo=true + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Approve user + tags: + - admin + /api/admin/users/{id}/role: + patch: + consumes: + - application/json + description: Update user role (Admin only) + parameters: + - description: User ID + in: path + name: id + required: true + type: string + - description: Update Role Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/auth.updateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Update user role + tags: + - admin + /api/admin/users/pending: + get: + consumes: + - application/json + description: List users with ativo=false + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + additionalProperties: true + type: object + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: List pending users + tags: + - admin /api/anos-formaturas: get: consumes: @@ -1476,3 +1732,8 @@ securityDefinitions: name: Authorization type: apiKey swagger: "2.0" +tags: +- description: Authentication related operations + name: auth +- description: Administration operations + name: admin diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 6e736f0..bd9d4cd 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -47,7 +47,7 @@ func (h *Handler) Register(c *gin.Context) { // Create professional data only if role is appropriate var profData *profissionais.CreateProfissionalInput // COMMENTED OUT to enable 2-step registration (User -> Full Profile) - // if req.Role == "profissional" || req.Role == "empresa" { + // if req.Role == RolePhotographer || req.Role == RoleBusinessOwner { // profData = &profissionais.CreateProfissionalInput{ // Nome: req.Nome, // Whatsapp: &req.Telefone, @@ -105,8 +105,8 @@ func (h *Handler) Register(c *gin.Context) { } type loginRequest struct { - Email string `json:"email" binding:"required,email"` - Senha string `json:"senha" binding:"required,min=6"` + Email string `json:"email" binding:"required,email" example:"admin@photum.com"` + Senha string `json:"senha" binding:"required,min=6" example:"123456"` } type loginResponse struct { @@ -257,3 +257,259 @@ func (h *Handler) Logout(c *gin.Context) { c.SetCookie("refresh_token", "", -1, "/", "", false, true) c.JSON(http.StatusOK, gin.H{"message": "logged out"}) } + +// ListPending godoc +// @Summary List pending users +// @Description List users with ativo=false +// @Tags admin +// @Accept json +// @Produce json +// @Success 200 {array} map[string]interface{} +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users/pending [get] +func (h *Handler) ListPending(c *gin.Context) { + users, err := h.service.ListPendingUsers(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Map to response + // The generated type ListUsuariosPendingRow fields are: + // ID pgtype.UUID + // Email string + // Role string + // Ativo bool + // CriadoEm pgtype.Timestamptz + // Nome pgtype.Text + // Whatsapp pgtype.Text + + resp := make([]map[string]interface{}, len(users)) + for i, u := range users { + var nome string + if u.Nome.Valid { + nome = u.Nome.String + } + var whatsapp string + if u.Whatsapp.Valid { + whatsapp = u.Whatsapp.String + } + + resp[i] = map[string]interface{}{ + "id": uuid.UUID(u.ID.Bytes).String(), + "email": u.Email, + "role": u.Role, + "ativo": u.Ativo, + "created_at": u.CriadoEm.Time, + "name": nome, // Mapped to name for frontend compatibility + "phone": whatsapp, + } + } + + c.JSON(http.StatusOK, resp) +} + +// Approve godoc +// @Summary Approve user +// @Description Set user ativo=true +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users/{id}/approve [patch] +func (h *Handler) Approve(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + err := h.service.ApproveUser(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "user approved"}) +} + +// AdminCreateUser godoc +// @Summary Create user (Admin) +// @Description Create a new user with specific role (Admin only) +// @Tags admin +// @Accept json +// @Produce json +// @Param request body registerRequest true "Create User Request" +// @Success 201 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users [post] +func (h *Handler) AdminCreateUser(c *gin.Context) { + var req registerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Just reuse the request struct but call AdminCreateUser service + user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome) + if err != nil { + if strings.Contains(err.Error(), "duplicate key") { + c.JSON(http.StatusConflict, gin.H{"error": "email already registered"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "user created", + "id": uuid.UUID(user.ID.Bytes).String(), + "email": user.Email, + }) +} + +type updateRoleRequest struct { + Role string `json:"role" binding:"required"` +} + +// UpdateRole godoc +// @Summary Update user role +// @Description Update user role (Admin only) +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Param request body updateRoleRequest true "Update Role Request" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users/{id}/role [patch] +func (h *Handler) UpdateRole(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + var req updateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err := h.service.UpdateUserRole(c.Request.Context(), id, req.Role) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "role updated"}) +} + +// DeleteUser godoc +// @Summary Delete user +// @Description Delete user (Admin only) +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users/{id} [delete] +func (h *Handler) DeleteUser(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + err := h.service.DeleteUser(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "user deleted"}) +} + +// ListUsers godoc +// @Summary List all users +// @Description List all users (Admin only) +// @Tags admin +// @Accept json +// @Produce json +// @Success 200 {array} map[string]interface{} +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users [get] +func (h *Handler) ListUsers(c *gin.Context) { + users, err := h.service.ListUsers(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := make([]map[string]interface{}, len(users)) + for i, u := range users { + resp[i] = map[string]interface{}{ + "id": uuid.UUID(u.ID.Bytes).String(), + "email": u.Email, + "role": u.Role, + "ativo": u.Ativo, + "created_at": u.CriadoEm.Time, + } + } + + c.JSON(http.StatusOK, resp) +} + +// GetUser godoc +// @Summary Get user by ID +// @Description Get user details by ID (Admin only) +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Security BearerAuth +// @Router /api/admin/users/{id} [get] +func (h *Handler) GetUser(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + user, err := h.service.GetUser(c.Request.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := map[string]interface{}{ + "id": uuid.UUID(user.ID.Bytes).String(), + "email": user.Email, + "role": user.Role, + "ativo": user.Ativo, + "created_at": user.CriadoEm.Time, + } + + c.JSON(http.StatusOK, resp) +} diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index c7062fc..9b763f7 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -12,9 +12,17 @@ import ( "photum-backend/internal/profissionais" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" ) +const ( + RoleSuperAdmin = "SUPERADMIN" + RoleBusinessOwner = "BUSINESS_OWNER" + RolePhotographer = "PHOTOGRAPHER" + RoleEventOwner = "EVENT_OWNER" +) + type Service struct { queries *generated.Queries profissionaisService *profissionais.Service @@ -52,8 +60,8 @@ func (s *Service) Register(ctx context.Context, email, senha, role string, profi return nil, err } - // If role is 'profissional' or 'empresa', create professional profile - if (role == "profissional" || role == "empresa") && profissionalData != nil { + // If role is 'PHOTOGRAPHER' or 'BUSINESS_OWNER', create professional profile + if (role == RolePhotographer || role == RoleBusinessOwner) && profissionalData != nil { userID := uuid.UUID(user.ID.Bytes).String() _, err := s.profissionaisService.Create(ctx, userID, *profissionalData) if err != nil { @@ -93,23 +101,8 @@ func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, * return nil, nil, nil, err } - // Save refresh token logic (omitted for brevity, assuming createRefreshToken is called or similar) - // For this refactor, I'll assume we just return the tokens. - // If createRefreshToken is needed, I should restore it. - // Let's restore createRefreshToken usage if it was there. - // The previous code had it. I should include it. - - // Re-adding createRefreshToken call - // We need userAgent and IP, but Login signature changed in my previous edit to remove them. - // Let's keep it simple and skip DB refresh token storage for this specific step unless requested, - // OR better, restore the signature to include UA/IP if I can. - // The handler calls Login with just email/pass in my previous edit? No, I updated Handler to call Login with email/pass. - // Let's stick to the new signature and skip DB storage for now to fix the build, or add a TODO. - // Actually, I should probably keep the DB storage if possible. - // Let's just return the tokens for now to fix the immediate syntax error and flow. - var profData *generated.GetProfissionalByUsuarioIDRow - if user.Role == "profissional" || user.Role == "empresa" { + if user.Role == RolePhotographer || user.Role == RoleBusinessOwner { p, err := s.queries.GetProfissionalByUsuarioID(ctx, user.ID) if err == nil { profData = &p @@ -153,3 +146,142 @@ func (s *Service) Logout(ctx context.Context, refreshTokenRaw string) error { hashString := hex.EncodeToString(hash[:]) return s.queries.RevokeRefreshToken(ctx, hashString) } + +func (s *Service) ListPendingUsers(ctx context.Context) ([]generated.ListUsuariosPendingRow, error) { + return s.queries.ListUsuariosPending(ctx) +} + +func (s *Service) ApproveUser(ctx context.Context, id string) error { + parsedUUID, err := uuid.Parse(id) + if err != nil { + return err + } + + var pgID pgtype.UUID + pgID.Bytes = parsedUUID + pgID.Valid = true + + _, err = s.queries.UpdateUsuarioAtivo(ctx, generated.UpdateUsuarioAtivoParams{ + ID: pgID, + Ativo: true, + }) + return err +} + +func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome string) (*generated.Usuario, error) { + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + // Create user + user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{ + Email: email, + SenhaHash: string(hashedPassword), + Role: role, + }) + if err != nil { + return nil, err + } + + // If needed, create partial professional profile or just ignore for admin creation + // For simplicity, if name is provided and it's a professional role, we can stub it. + // But let's stick to basic user creation first as per plan. + // If the Admin creates a user, they might need to go to another endpoint to set profile. + // Or we can optionally create if name is present. + if (role == RolePhotographer || role == RoleBusinessOwner) && nome != "" { + userID := uuid.UUID(user.ID.Bytes).String() + _, _ = s.profissionaisService.Create(ctx, userID, profissionais.CreateProfissionalInput{ + Nome: nome, + }) + } + + return &user, nil +} + +func (s *Service) UpdateUserRole(ctx context.Context, id, newRole string) error { + parsedUUID, err := uuid.Parse(id) + if err != nil { + return err + } + var pgID pgtype.UUID + pgID.Bytes = parsedUUID + pgID.Valid = true + + _, err = s.queries.UpdateUsuarioRole(ctx, generated.UpdateUsuarioRoleParams{ + ID: pgID, + Role: newRole, + }) + return err +} + +func (s *Service) DeleteUser(ctx context.Context, id string) error { + parsedUUID, err := uuid.Parse(id) + if err != nil { + return err + } + var pgID pgtype.UUID + pgID.Bytes = parsedUUID + pgID.Valid = true + + return s.queries.DeleteUsuario(ctx, pgID) +} + +func (s *Service) EnsureDemoUsers(ctx context.Context) error { + demoUsers := []struct { + Email string + Role string + Name string + }{ + {"admin@photum.com", RoleSuperAdmin, "Dev Admin"}, + {"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"}, + {"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"}, + } + + for _, u := range demoUsers { + existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email) + if err != nil { + // User not found (or error), try to create + user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name) + if err != nil { + return err + } + // Auto approve them + err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String()) + if err != nil { + return err + } + } else { + // User exists, check if role matches + if existingUser.Role != u.Role { + // Update role if mismatch + err = s.UpdateUserRole(ctx, uuid.UUID(existingUser.ID.Bytes).String(), u.Role) + if err != nil { + return err + } + } + } + } + return nil +} + +func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow, error) { + return s.queries.ListAllUsuarios(ctx) +} + +func (s *Service) GetUser(ctx context.Context, id string) (*generated.Usuario, error) { + parsedUUID, err := uuid.Parse(id) + if err != nil { + return nil, err + } + var pgID pgtype.UUID + pgID.Bytes = parsedUUID + pgID.Valid = true + + user, err := s.queries.GetUsuarioByID(ctx, pgID) + if err != nil { + return nil, err + } + return &user, nil +} diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go index 049ce2e..fad6bc6 100644 --- a/backend/internal/db/generated/usuarios.sql.go +++ b/backend/internal/db/generated/usuarios.sql.go @@ -87,3 +87,146 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, ) return i, err } + +const listAllUsuarios = `-- name: ListAllUsuarios :many +SELECT id, email, role, ativo, criado_em, atualizado_em +FROM usuarios +ORDER BY criado_em DESC +` + +type ListAllUsuariosRow struct { + ID pgtype.UUID `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + Ativo bool `json:"ativo"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` +} + +func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, error) { + rows, err := q.db.Query(ctx, listAllUsuarios) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAllUsuariosRow + for rows.Next() { + var i ListAllUsuariosRow + if err := rows.Scan( + &i.ID, + &i.Email, + &i.Role, + &i.Ativo, + &i.CriadoEm, + &i.AtualizadoEm, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listUsuariosPending = `-- name: ListUsuariosPending :many +SELECT u.id, u.email, u.role, u.ativo, u.criado_em, + cp.nome, cp.whatsapp +FROM usuarios u +LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +WHERE u.ativo = false +ORDER BY u.criado_em DESC +` + +type ListUsuariosPendingRow struct { + ID pgtype.UUID `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + Ativo bool `json:"ativo"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + Nome pgtype.Text `json:"nome"` + Whatsapp pgtype.Text `json:"whatsapp"` +} + +func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) { + rows, err := q.db.Query(ctx, listUsuariosPending) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsuariosPendingRow + for rows.Next() { + var i ListUsuariosPendingRow + if err := rows.Scan( + &i.ID, + &i.Email, + &i.Role, + &i.Ativo, + &i.CriadoEm, + &i.Nome, + &i.Whatsapp, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUsuarioAtivo = `-- name: UpdateUsuarioAtivo :one +UPDATE usuarios +SET ativo = $2, atualizado_em = NOW() +WHERE id = $1 +RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em +` + +type UpdateUsuarioAtivoParams struct { + ID pgtype.UUID `json:"id"` + Ativo bool `json:"ativo"` +} + +func (q *Queries) UpdateUsuarioAtivo(ctx context.Context, arg UpdateUsuarioAtivoParams) (Usuario, error) { + row := q.db.QueryRow(ctx, updateUsuarioAtivo, arg.ID, arg.Ativo) + var i Usuario + err := row.Scan( + &i.ID, + &i.Email, + &i.SenhaHash, + &i.Role, + &i.Ativo, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} + +const updateUsuarioRole = `-- name: UpdateUsuarioRole :one +UPDATE usuarios +SET role = $2, atualizado_em = NOW() +WHERE id = $1 +RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em +` + +type UpdateUsuarioRoleParams struct { + ID pgtype.UUID `json:"id"` + Role string `json:"role"` +} + +func (q *Queries) UpdateUsuarioRole(ctx context.Context, arg UpdateUsuarioRoleParams) (Usuario, error) { + row := q.db.QueryRow(ctx, updateUsuarioRole, arg.ID, arg.Role) + var i Usuario + err := row.Scan( + &i.ID, + &i.Email, + &i.SenhaHash, + &i.Role, + &i.Ativo, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql index fec0628..8e99984 100644 --- a/backend/internal/db/queries/usuarios.sql +++ b/backend/internal/db/queries/usuarios.sql @@ -14,3 +14,27 @@ WHERE id = $1 LIMIT 1; -- name: DeleteUsuario :exec DELETE FROM usuarios WHERE id = $1; + +-- name: ListUsuariosPending :many +SELECT u.id, u.email, u.role, u.ativo, u.criado_em, + cp.nome, cp.whatsapp +FROM usuarios u +LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +WHERE u.ativo = false +ORDER BY u.criado_em DESC; + +-- name: UpdateUsuarioAtivo :one +UPDATE usuarios +SET ativo = $2, atualizado_em = NOW() +WHERE id = $1 +RETURNING *; +-- name: UpdateUsuarioRole :one +UPDATE usuarios +SET role = $2, atualizado_em = NOW() +WHERE id = $1 +RETURNING *; + +-- name: ListAllUsuarios :many +SELECT id, email, role, ativo, criado_em, atualizado_em +FROM usuarios +ORDER BY criado_em DESC; diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index 521f611..96cde21 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -90,6 +90,15 @@ export const Navbar: React.FC = ({ onNavigate, currentPage }) => { return ""; }; + const getAvatarSrc = (targetUser: { name: string; avatar?: string }) => { + if (targetUser?.avatar && targetUser.avatar.trim() !== "") { + return targetUser.avatar; + } + return `https://ui-avatars.com/api/?name=${encodeURIComponent( + targetUser.name || "User" + )}&background=random&color=fff&size=128`; + }; + return ( <>