Merge pull request 22 from rede5/Front-back-integracao-task4 - Feature/Admin Users & Melhorias no Cadastro

implementa endpoints administrativos no backend para gerenciamento de usuários, padroniza os níveis de acesso (roles) em toda a aplicação para resolver inconsistências e melhora significativamente a experiência do usuário (UX) nos formulários de cadastro com textos de ajuda e preenchimento automático de endereço.

Principais Mudanças

Backend

Endpoints Admin: Implementação de queries, serviço e handlers para as rotas GET /api/admin/users e GET /api/admin/users/:id.
Padronização de Roles: Refatoração do serviço de auth para usar constantes padronizadas (SUPERADMIN, BUSINESS_OWNER, PHOTOGRAPHER, EVENT_OWNER) ao invés de strings hardcoded, garantindo alinhamento com os tipos do Frontend.
Usuários Demo: Atualização do 
EnsureDemoUsers
 para migrar automaticamente os usuários de demonstração existentes para o novo formato de role ao iniciar a aplicação.
Docs: Swagger atualizado.
Frontend

Cadastro de Cliente (/cadastro):
Adicionado texto de ajuda acima do campo de seleção de Empresa orientando usuários que não encontram sua instituição.
Implementada ordenação personalizada para forçar "Não Cadastrado" a aparecer sempre no topo da lista.
Cadastro de Profissional (/cadastro-profissional):
Adicionado campo de CEP com integração à API AwesomeAPI.
Implementada lógica para preencher automaticamente Rua, Bairro, Cidade e UF ao sair do campo de CEP (blur).
Atualizado o payload para incluir o CEP na string de endereço enviada ao backend.
Como Testar

Roles do Backend: Inicie o backend e faça login com usuários demo (ex: admin@photum.com); verifique se as roles agora são SUPERADMIN, etc.
API Admin: Use o Swagger para chamar GET /api/admin/users (requer token de Admin).
Cadastro: Acesse /cadastro e verifique a ordenação da lista de empresas.
Formulário Profissional: Acesse /cadastro-profissional, digite um CEP válido (ex: 01001-000) e verifique se os campos de endereço são preenchidos automaticamente.
This commit is contained in:
Andre F. Rodrigues 2025-12-15 11:21:15 -03:00 committed by GitHub
commit 3011a0634f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2059 additions and 198 deletions

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"log" "log"
"photum-backend/docs" "photum-backend/docs"
@ -22,8 +23,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
// "photum-backend/docs" is already imported above
_ "photum-backend/docs" // Import generated docs
) )
// @title Photum Backend API // @title Photum Backend API
@ -40,6 +40,10 @@ import (
// @host localhost:8080 // @host localhost:8080
// @BasePath / // @BasePath /
// @tag.name auth
// @tag.description Authentication related operations
// @tag.name admin
// @tag.description Administration operations
// @securityDefinitions.apikey BearerAuth // @securityDefinitions.apikey BearerAuth
// @in header // @in header
@ -65,6 +69,11 @@ func main() {
tiposEventosService := tipos_eventos.NewService(queries) tiposEventosService := tipos_eventos.NewService(queries)
cadastroFotService := cadastro_fot.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 // Initialize handlers
authHandler := auth.NewHandler(authService) authHandler := auth.NewHandler(authService)
profissionaisHandler := profissionais.NewHandler(profissionaisService) profissionaisHandler := profissionais.NewHandler(profissionaisService)
@ -88,7 +97,6 @@ func main() {
configCors.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} configCors.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"}
r.Use(cors.New(configCors)) r.Use(cors.New(configCors))
// Swagger
// Swagger // Swagger
// Dynamically update Swagger Info // Dynamically update Swagger Info
docs.SwaggerInfo.Host = cfg.SwaggerHost docs.SwaggerInfo.Host = cfg.SwaggerHost
@ -98,7 +106,13 @@ func main() {
docs.SwaggerInfo.Schemes = []string{"http", "https"} 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 // Public Routes
authGroup := r.Group("/auth") authGroup := r.Group("/auth")
@ -175,6 +189,17 @@ func main() {
api.GET("/cadastro-fot/:id", cadastroFotHandler.Get) api.GET("/cadastro-fot/:id", cadastroFotHandler.Get)
api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update) api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update)
api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete) 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) log.Printf("Swagger Host Configured: %s", cfg.SwaggerHost)

View file

@ -24,6 +24,392 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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": { "/api/anos-formaturas": {
"get": { "get": {
"security": [ "security": [
@ -1786,11 +2172,13 @@ const docTemplate = `{
], ],
"properties": { "properties": {
"email": { "email": {
"type": "string" "type": "string",
"example": "admin@photum.com"
}, },
"senha": { "senha": {
"type": "string", "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": { "auth.userResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2325,7 +2724,17 @@ const docTemplate = `{
"name": "Authorization", "name": "Authorization",
"in": "header" "in": "header"
} }
},
"tags": [
{
"description": "Authentication related operations",
"name": "auth"
},
{
"description": "Administration operations",
"name": "admin"
} }
]
}` }`
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it

View file

@ -18,6 +18,392 @@
"host": "localhost:8080", "host": "localhost:8080",
"basePath": "/", "basePath": "/",
"paths": { "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": { "/api/anos-formaturas": {
"get": { "get": {
"security": [ "security": [
@ -1780,11 +2166,13 @@
], ],
"properties": { "properties": {
"email": { "email": {
"type": "string" "type": "string",
"example": "admin@photum.com"
}, },
"senha": { "senha": {
"type": "string", "type": "string",
"minLength": 6 "minLength": 6,
"example": "123456"
} }
} }
}, },
@ -1831,6 +2219,17 @@
} }
} }
}, },
"auth.updateRoleRequest": {
"type": "object",
"required": [
"role"
],
"properties": {
"role": {
"type": "string"
}
}
},
"auth.userResponse": { "auth.userResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2319,5 +2718,15 @@
"name": "Authorization", "name": "Authorization",
"in": "header" "in": "header"
} }
},
"tags": [
{
"description": "Authentication related operations",
"name": "auth"
},
{
"description": "Administration operations",
"name": "admin"
} }
]
} }

View file

@ -18,8 +18,10 @@ definitions:
auth.loginRequest: auth.loginRequest:
properties: properties:
email: email:
example: admin@photum.com
type: string type: string
senha: senha:
example: "123456"
minLength: 6 minLength: 6
type: string type: string
required: required:
@ -56,6 +58,13 @@ definitions:
- role - role
- senha - senha
type: object type: object
auth.updateRoleRequest:
properties:
role:
type: string
required:
- role
type: object
auth.userResponse: auth.userResponse:
properties: properties:
ativo: ativo:
@ -385,6 +394,253 @@ info:
title: Photum Backend API title: Photum Backend API
version: "1.0" version: "1.0"
paths: 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: /api/anos-formaturas:
get: get:
consumes: consumes:
@ -1476,3 +1732,8 @@ securityDefinitions:
name: Authorization name: Authorization
type: apiKey type: apiKey
swagger: "2.0" swagger: "2.0"
tags:
- description: Authentication related operations
name: auth
- description: Administration operations
name: admin

View file

@ -47,7 +47,7 @@ func (h *Handler) Register(c *gin.Context) {
// Create professional data only if role is appropriate // Create professional data only if role is appropriate
var profData *profissionais.CreateProfissionalInput var profData *profissionais.CreateProfissionalInput
// COMMENTED OUT to enable 2-step registration (User -> Full Profile) // 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{ // profData = &profissionais.CreateProfissionalInput{
// Nome: req.Nome, // Nome: req.Nome,
// Whatsapp: &req.Telefone, // Whatsapp: &req.Telefone,
@ -105,8 +105,8 @@ func (h *Handler) Register(c *gin.Context) {
} }
type loginRequest struct { type loginRequest struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email" example:"admin@photum.com"`
Senha string `json:"senha" binding:"required,min=6"` Senha string `json:"senha" binding:"required,min=6" example:"123456"`
} }
type loginResponse struct { type loginResponse struct {
@ -257,3 +257,259 @@ func (h *Handler) Logout(c *gin.Context) {
c.SetCookie("refresh_token", "", -1, "/", "", false, true) c.SetCookie("refresh_token", "", -1, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"message": "logged out"}) 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)
}

View file

@ -12,9 +12,17 @@ import (
"photum-backend/internal/profissionais" "photum-backend/internal/profissionais"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
const (
RoleSuperAdmin = "SUPERADMIN"
RoleBusinessOwner = "BUSINESS_OWNER"
RolePhotographer = "PHOTOGRAPHER"
RoleEventOwner = "EVENT_OWNER"
)
type Service struct { type Service struct {
queries *generated.Queries queries *generated.Queries
profissionaisService *profissionais.Service profissionaisService *profissionais.Service
@ -52,8 +60,8 @@ func (s *Service) Register(ctx context.Context, email, senha, role string, profi
return nil, err return nil, err
} }
// If role is 'profissional' or 'empresa', create professional profile // If role is 'PHOTOGRAPHER' or 'BUSINESS_OWNER', create professional profile
if (role == "profissional" || role == "empresa") && profissionalData != nil { if (role == RolePhotographer || role == RoleBusinessOwner) && profissionalData != nil {
userID := uuid.UUID(user.ID.Bytes).String() userID := uuid.UUID(user.ID.Bytes).String()
_, err := s.profissionaisService.Create(ctx, userID, *profissionalData) _, err := s.profissionaisService.Create(ctx, userID, *profissionalData)
if err != nil { if err != nil {
@ -93,23 +101,8 @@ func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, *
return nil, nil, nil, err 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 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) p, err := s.queries.GetProfissionalByUsuarioID(ctx, user.ID)
if err == nil { if err == nil {
profData = &p profData = &p
@ -153,3 +146,142 @@ func (s *Service) Logout(ctx context.Context, refreshTokenRaw string) error {
hashString := hex.EncodeToString(hash[:]) hashString := hex.EncodeToString(hash[:])
return s.queries.RevokeRefreshToken(ctx, hashString) 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
}

View file

@ -87,3 +87,146 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario,
) )
return i, err 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
}

View file

@ -14,3 +14,27 @@ WHERE id = $1 LIMIT 1;
-- name: DeleteUsuario :exec -- name: DeleteUsuario :exec
DELETE FROM usuarios DELETE FROM usuarios
WHERE id = $1; 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;

View file

@ -90,6 +90,15 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
return ""; 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 ( return (
<> <>
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3"> <nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
@ -114,8 +123,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<button <button
key={link.path} key={link.path}
onClick={() => onNavigate(link.path)} onClick={() => onNavigate(link.path)}
className={`text-xs xl:text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${ className={`text-xs xl:text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${currentPage === link.path
currentPage === link.path
? "text-brand-gold border-b-2 border-brand-gold" ? "text-brand-gold border-b-2 border-brand-gold"
: "text-gray-600 border-b-2 border-transparent" : "text-gray-600 border-b-2 border-transparent"
}`} }`}
@ -148,7 +156,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
className="h-8 w-8 xl:h-9 xl:w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer" className="h-8 w-8 xl:h-9 xl:w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer"
> >
<img <img
src={user.avatar} src={getAvatarSrc(user)}
alt="Avatar" alt="Avatar"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@ -161,7 +169,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center relative"> <div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center relative">
<div className="w-20 h-20 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden"> <div className="w-20 h-20 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden">
<img <img
src={user.avatar} src={getAvatarSrc(user)}
alt={user.name} alt={user.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@ -329,7 +337,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
className="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md" className="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md"
> >
<img <img
src={user.avatar} src={getAvatarSrc(user)}
alt="Avatar" alt="Avatar"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@ -342,7 +350,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center relative"> <div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center relative">
<div className="w-20 h-20 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden"> <div className="w-20 h-20 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden">
<img <img
src={user.avatar} src={getAvatarSrc(user)}
alt={user.name} alt={user.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@ -528,7 +536,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
<div className="flex items-center justify-between pb-3 border-b border-gray-100"> <div className="flex items-center justify-between pb-3 border-b border-gray-100">
<div className="flex items-center"> <div className="flex items-center">
<img <img
src={user.avatar} src={getAvatarSrc(user)}
className="w-10 h-10 rounded-full mr-3 border-2 border-gray-200" className="w-10 h-10 rounded-full mr-3 border-2 border-gray-200"
alt={user.name} alt={user.name}
/> />
@ -641,7 +649,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</button> </button>
<div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group"> <div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group">
<img <img
src={profileData.avatar} src={getAvatarSrc(profileData)}
alt="Avatar" alt="Avatar"
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />

View file

@ -14,6 +14,7 @@ export interface ProfessionalData {
senha: string; senha: string;
confirmarSenha: string; confirmarSenha: string;
funcaoId: string; funcaoId: string;
cep: string;
rua: string; rua: string;
numero: string; numero: string;
complemento: string; complemento: string;
@ -41,12 +42,14 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
>([]); >([]);
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false); const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
const [functionsError, setFunctionsError] = useState(false); const [functionsError, setFunctionsError] = useState(false);
const [isLoadingCep, setIsLoadingCep] = useState(false);
const [formData, setFormData] = useState<ProfessionalData>({ const [formData, setFormData] = useState<ProfessionalData>({
nome: "", nome: "",
email: "", email: "",
senha: "", senha: "",
confirmarSenha: "", confirmarSenha: "",
funcaoId: "", funcaoId: "",
cep: "",
rua: "", rua: "",
numero: "", numero: "",
complemento: "", complemento: "",
@ -88,6 +91,33 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleCepBlur = async () => {
const cep = formData.cep.replace(/\D/g, "");
if (cep.length !== 8) return;
setIsLoadingCep(true);
try {
const response = await fetch(
`https://cep.awesomeapi.com.br/json/${cep}`
);
if (!response.ok) throw new Error("CEP não encontrado");
const data = await response.json();
setFormData((prev) => ({
...prev,
rua: data.address || prev.rua,
bairro: data.district || prev.bairro,
cidade: data.city || prev.cidade,
uf: data.state || prev.uf,
}));
} catch (error) {
console.error("Erro ao buscar CEP:", error);
// Opcional: mostrar erro para usuário
} finally {
setIsLoadingCep(false);
}
};
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -248,6 +278,24 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
Endereço Endereço
</h3> </h3>
<Input
label="CEP *"
type="text"
required
placeholder="00000-000"
value={formData.cep}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, "");
const formatted = value.replace(/^(\d{5})(\d)/, "$1-$2");
handleChange("cep", formatted);
}}
onBlur={handleCepBlur}
maxLength={9}
/>
{isLoadingCep && (
<p className="text-xs text-blue-500">Buscando endereço...</p>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Input <Input

View file

@ -70,13 +70,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}; };
const login = async (email: string, password?: string) => { const login = async (email: string, password?: string) => {
// 1. Check for Demo/Mock users first // 1. Check for Demo/Mock users first - REMOVED to force API usage
const mockUser = MOCK_USERS.find(u => u.email === email); // const mockUser = MOCK_USERS.find(u => u.email === email);
if (mockUser) { // if (mockUser) {
await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay // await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay
setUser({ ...mockUser, ativo: true }); // setUser({ ...mockUser, ativo: true });
return true; // return true;
} // }
// 2. Try Real API // 2. Try Real API
try { try {
@ -94,9 +94,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const data = await response.json(); const data = await response.json();
// Store token (optional, if you need it for other requests outside cookies) // Store token (optional, if you need it for other requests outside cookies)
// localStorage.setItem('token', data.access_token); localStorage.setItem('token', data.access_token);
const backendUser = data.user; const backendUser = data.user;
// Enforce active check
if (!backendUser.ativo) {
throw new Error("Cadastro pendente de aprovação.");
}
// Map backend user to frontend User type // Map backend user to frontend User type
const mappedUser: User = { const mappedUser: User = {
id: backendUser.id, id: backendUser.id,
@ -111,11 +117,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return true; return true;
} catch (err) { } catch (err) {
console.error('Login error:', err); console.error('Login error:', err);
return false; throw err;
} }
}; };
const logout = () => { const logout = () => {
localStorage.removeItem('token');
setUser(null); setUser(null);
}; };
@ -135,8 +142,14 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const responseData = await response.json(); const responseData = await response.json();
// If user is returned (auto-login), set state if (responseData.access_token) {
if (responseData.user) { localStorage.setItem('token', responseData.access_token);
}
// IF user is returned (auto-login), logic:
// Only set user if they are ACTIVE (which they won't be for standard clients)
// This allows the "Pending Approval" modal to show instead of auto-redirecting.
if (responseData.user && responseData.user.ativo) {
const backendUser = responseData.user; const backendUser = responseData.user;
const mappedUser: User = { const mappedUser: User = {
id: backendUser.id, id: backendUser.id,

View file

@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, ReactNode } from "react"; import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
import { getPendingUsers, approveUser as apiApproveUser } from "../services/apiService";
import { import {
EventData, EventData,
EventStatus, EventStatus,
@ -615,6 +616,43 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES); const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
const [pendingUsers, setPendingUsers] = useState<User[]>([]); const [pendingUsers, setPendingUsers] = useState<User[]>([]);
// Fetch pending users from API
useEffect(() => {
const fetchUsers = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const result = await getPendingUsers(token);
if (result.data) {
const mappedUsers: User[] = result.data.map((u: any) => {
// Map backend roles to frontend enum
let mappedRole = UserRole.EVENT_OWNER;
if (u.role === 'profissional') mappedRole = UserRole.PHOTOGRAPHER;
else if (u.role === 'empresa') mappedRole = UserRole.BUSINESS_OWNER;
else if (u.role === 'admin') mappedRole = UserRole.SUPERADMIN;
else if (u.role === 'cliente') mappedRole = UserRole.EVENT_OWNER;
return {
id: u.id,
name: u.name || u.email.split('@')[0],
email: u.email,
phone: u.phone || '',
role: mappedRole,
approvalStatus: UserApprovalStatus.PENDING,
createdAt: u.created_at,
registeredInstitution: mappedRole === UserRole.EVENT_OWNER ? 'N/A' : undefined,
};
});
setPendingUsers(mappedUsers);
}
} catch (error) {
console.error("Failed to fetch pending users", error);
}
}
};
fetchUsers();
}, []);
const addEvent = (event: EventData) => { const addEvent = (event: EventData) => {
setEvents((prev) => [event, ...prev]); setEvents((prev) => [event, ...prev]);
}; };
@ -715,7 +753,17 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
setPendingUsers((prev) => [...prev, newUser]); setPendingUsers((prev) => [...prev, newUser]);
}; };
const approveUser = (userId: string) => { const approveUser = async (userId: string) => {
const token = localStorage.getItem('token');
if (token) {
try {
await apiApproveUser(userId, token);
setPendingUsers((prev) => prev.filter(user => user.id !== userId));
} catch (error) {
console.error("Failed to approve user", error);
}
} else {
// Fallback for mock/testing
setPendingUsers((prev) => setPendingUsers((prev) =>
prev.map((user) => prev.map((user) =>
user.id === userId user.id === userId
@ -723,6 +771,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
: user : user
) )
); );
}
}; };
const rejectUser = (userId: string) => { const rejectUser = (userId: string) => {

View file

@ -197,7 +197,11 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
Usuários de Demonstração (Clique para preencher) Usuários de Demonstração (Clique para preencher)
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
{availableUsers.map((user) => ( {[
{ id: "1", name: "Dev Admin", email: "admin@photum.com", role: UserRole.SUPERADMIN },
{ id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER },
{ id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER },
].map((user) => (
<button <button
key={user.id} key={user.id}
onClick={() => fillCredentials(user.email)} onClick={() => fillCredentials(user.email)}

View file

@ -43,7 +43,7 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
cidade: professionalData.cidade, cidade: professionalData.cidade,
conta_pix: professionalData.contaPix, conta_pix: professionalData.contaPix,
cpf_cnpj_titular: professionalData.cpfCnpj, cpf_cnpj_titular: professionalData.cpfCnpj,
endereco: `${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`, endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`,
equipamentos: "", // Campo não está no form explícito, talvez observação ou outro? equipamentos: "", // Campo não está no form explícito, talvez observação ou outro?
extra_por_equipamento: false, // Default extra_por_equipamento: false, // Default
funcao_profissional_id: professionalData.funcaoId, funcao_profissional_id: professionalData.funcaoId,
@ -85,8 +85,8 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
className="min-h-screen flex items-center justify-center" className="min-h-screen flex items-center justify-center"
style={{ backgroundColor: "#B9CF33" }} style={{ backgroundColor: "#B9CF33" }}
> >
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 text-center"> <div className="text-center fade-in max-w-md px-4 py-8 bg-white rounded-2xl shadow-xl">
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="w-16 h-16 bg-yellow-500 rounded-full flex items-center justify-center mx-auto mb-4">
<svg <svg
className="w-8 h-8 text-white" className="w-8 h-8 text-white"
fill="none" fill="none"
@ -97,23 +97,30 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d="M5 13l4 4L19 7" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"> <h2 className="text-2xl font-bold text-gray-900 mb-2">
Cadastro Realizado! Cadastro Pendente de Aprovação
</h2> </h2>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 mb-4">
Seu cadastro de profissional foi realizado com sucesso e está Seu cadastro foi realizado com sucesso e está aguardando aprovação.
aguardando aprovação.
</p> </p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-sm text-yellow-800">
<strong>Atenção:</strong> Enquanto seu cadastro não for aprovado,
você não terá acesso ao sistema.
</p>
</div>
<button <button
onClick={() => onNavigate("entrar")} onClick={() => {
className="w-full px-6 py-3 text-white font-semibold rounded-lg transition-colors" window.location.href = "/";
}}
className="w-full px-6 py-2 text-white font-semibold rounded-lg hover:bg-opacity-90 transition-colors"
style={{ backgroundColor: "#B9CF33" }} style={{ backgroundColor: "#B9CF33" }}
> >
Ir para Login OK
</button> </button>
</div> </div>
</div> </div>

View file

@ -122,20 +122,12 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
</div> </div>
<button <button
onClick={() => { onClick={() => {
setIsPending(false); window.location.href = "/";
setFormData({
name: "",
email: "",
phone: "",
password: "",
confirmPassword: "",
empresaId: "",
});
setAgreedToTerms(false);
}} }}
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors" className="w-full px-6 py-2 text-white font-semibold rounded-lg hover:bg-opacity-90 transition-colors"
style={{ backgroundColor: "#B9CF33" }}
> >
Fazer Novo Cadastro OK
</button> </button>
</div> </div>
</div> </div>
@ -232,6 +224,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
<label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1"> <label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1">
Empresa * Empresa *
</label> </label>
<p className="text-[10px] text-gray-500 mb-2 leading-tight">
Se a sua empresa não estiver listada, entre em contato com a
administração e selecione <strong>"Não Cadastrado"</strong>{" "}
abaixo.
</p>
{isLoadingCompanies ? ( {isLoadingCompanies ? (
<p className="text-xs sm:text-sm text-gray-500"> <p className="text-xs sm:text-sm text-gray-500">
Carregando empresas... Carregando empresas...
@ -245,7 +242,15 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
style={{ focusRing: "2px solid #B9CF33" }} style={{ focusRing: "2px solid #B9CF33" }}
> >
<option value="">Selecione uma empresa</option> <option value="">Selecione uma empresa</option>
{companies.map((company) => ( {companies
.sort((a, b) => {
const nameA = a.nome.toLowerCase();
const nameB = b.nome.toLowerCase();
if (nameA.includes("não cadastrado") || nameA.includes("nao cadastrado")) return -1;
if (nameB.includes("não cadastrado") || nameB.includes("nao cadastrado")) return 1;
return nameA.localeCompare(nameB);
})
.map((company) => (
<option key={company.id} value={company.id}> <option key={company.id} value={company.id}>
{company.nome} {company.nome}
</option> </option>

View file

@ -194,3 +194,71 @@ export async function getUniversities(): Promise<
> { > {
return fetchFromBackend("/api/universidades"); return fetchFromBackend("/api/universidades");
} }
// ... existing functions ...
/**
* Busca usuários pendentes de aprovação
*/
export async function getPendingUsers(token: string): Promise<ApiResponse<any[]>> {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/pending`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
data,
error: null,
isBackendDown: false,
};
} catch (error) {
console.error("Error fetching pending users:", error);
return {
data: null,
error: error instanceof Error ? error.message : "Erro desconhecido",
isBackendDown: true,
};
}
}
/**
* Aprova um usuário
*/
export async function approveUser(userId: string, token: string): Promise<ApiResponse<any>> {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/approve`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
data,
error: null,
isBackendDown: false,
};
} catch (error) {
console.error("Error approving user:", error);
return {
data: null,
error: error instanceof Error ? error.message : "Erro desconhecido",
isBackendDown: true,
};
}
}