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:
commit
3011a0634f
16 changed files with 2059 additions and 198 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,15 @@ export const Navbar: React.FC<NavbarProps> = ({ 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 (
|
||||
<>
|
||||
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
|
||||
|
|
@ -114,11 +123,10 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
<button
|
||||
key={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 ${
|
||||
currentPage === link.path
|
||||
? "text-brand-gold border-b-2 border-brand-gold"
|
||||
: "text-gray-600 border-b-2 border-transparent"
|
||||
}`}
|
||||
className={`text-xs xl:text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${currentPage === link.path
|
||||
? "text-brand-gold border-b-2 border-brand-gold"
|
||||
: "text-gray-600 border-b-2 border-transparent"
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
src={getAvatarSrc(user)}
|
||||
alt="Avatar"
|
||||
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="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
|
||||
src={user.avatar}
|
||||
src={getAvatarSrc(user)}
|
||||
alt={user.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
|
@ -182,50 +190,50 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||
user.role === UserRole.EVENT_OWNER) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center group-hover:bg-[#492E61]/20 transition-colors">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center group-hover:bg-[#492E61]/20 transition-colors">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Configurações - Apenas para CEO e Business Owner */}
|
||||
{(user.role === UserRole.BUSINESS_OWNER ||
|
||||
user.role === UserRole.SUPERADMIN) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigate("configuracoes");
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
||||
<Settings size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Configurações
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Preferências da conta
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigate("configuracoes");
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
||||
<Settings size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Configurações
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Preferências da conta
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Sair */}
|
||||
<button
|
||||
|
|
@ -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"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
src={getAvatarSrc(user)}
|
||||
alt="Avatar"
|
||||
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="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
|
||||
src={user.avatar}
|
||||
src={getAvatarSrc(user)}
|
||||
alt={user.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
|
@ -363,50 +371,50 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||
user.role === UserRole.EVENT_OWNER) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center group-hover:bg-[#492E61]/20 transition-colors">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center group-hover:bg-[#492E61]/20 transition-colors">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Configurações - Apenas para CEO e Business Owner */}
|
||||
{(user.role === UserRole.BUSINESS_OWNER ||
|
||||
user.role === UserRole.SUPERADMIN) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigate("configuracoes");
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
||||
<Settings size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Configurações
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Preferências da conta
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigate("configuracoes");
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
||||
<Settings size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Configurações
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Preferências da conta
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Sair */}
|
||||
<button
|
||||
|
|
@ -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">
|
||||
<img
|
||||
src={user.avatar}
|
||||
src={getAvatarSrc(user)}
|
||||
className="w-10 h-10 rounded-full mr-3 border-2 border-gray-200"
|
||||
alt={user.name}
|
||||
/>
|
||||
|
|
@ -546,26 +554,26 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{/* Botão Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||
user.role === UserRole.EVENT_OWNER) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 bg-[#492E61]/5 hover:bg-[#492E61]/10 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditProfileModalOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 bg-[#492E61]/5 hover:bg-[#492E61]/10 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center">
|
||||
<User size={20} className="text-[#492E61]" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
Editar Perfil
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Atualize suas informações
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Botão Sair */}
|
||||
<button
|
||||
|
|
@ -641,7 +649,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
</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">
|
||||
<img
|
||||
src={profileData.avatar}
|
||||
src={getAvatarSrc(profileData)}
|
||||
alt="Avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface ProfessionalData {
|
|||
senha: string;
|
||||
confirmarSenha: string;
|
||||
funcaoId: string;
|
||||
cep: string;
|
||||
rua: string;
|
||||
numero: string;
|
||||
complemento: string;
|
||||
|
|
@ -41,12 +42,14 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
|||
>([]);
|
||||
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
|
||||
const [functionsError, setFunctionsError] = useState(false);
|
||||
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
||||
const [formData, setFormData] = useState<ProfessionalData>({
|
||||
nome: "",
|
||||
email: "",
|
||||
senha: "",
|
||||
confirmarSenha: "",
|
||||
funcaoId: "",
|
||||
cep: "",
|
||||
rua: "",
|
||||
numero: "",
|
||||
complemento: "",
|
||||
|
|
@ -88,6 +91,33 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
|||
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) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -248,6 +278,24 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
|||
Endereço
|
||||
</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="sm:col-span-2">
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
};
|
||||
|
||||
const login = async (email: string, password?: string) => {
|
||||
// 1. Check for Demo/Mock users first
|
||||
const mockUser = MOCK_USERS.find(u => u.email === email);
|
||||
if (mockUser) {
|
||||
await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay
|
||||
setUser({ ...mockUser, ativo: true });
|
||||
return true;
|
||||
}
|
||||
// 1. Check for Demo/Mock users first - REMOVED to force API usage
|
||||
// const mockUser = MOCK_USERS.find(u => u.email === email);
|
||||
// if (mockUser) {
|
||||
// await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay
|
||||
// setUser({ ...mockUser, ativo: true });
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// 2. Try Real API
|
||||
try {
|
||||
|
|
@ -94,9 +94,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
const data = await response.json();
|
||||
|
||||
// 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;
|
||||
|
||||
// Enforce active check
|
||||
if (!backendUser.ativo) {
|
||||
throw new Error("Cadastro pendente de aprovação.");
|
||||
}
|
||||
|
||||
// Map backend user to frontend User type
|
||||
const mappedUser: User = {
|
||||
id: backendUser.id,
|
||||
|
|
@ -111,11 +117,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
return true;
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
return false;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
|
|
@ -135,8 +142,14 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
|
||||
const responseData = await response.json();
|
||||
|
||||
// If user is returned (auto-login), set state
|
||||
if (responseData.user) {
|
||||
if (responseData.access_token) {
|
||||
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 mappedUser: User = {
|
||||
id: backendUser.id,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
EventData,
|
||||
EventStatus,
|
||||
|
|
@ -615,6 +616,43 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
|
||||
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) => {
|
||||
setEvents((prev) => [event, ...prev]);
|
||||
};
|
||||
|
|
@ -695,12 +733,12 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
};
|
||||
|
||||
// Funções para gerenciar usuários pendentes
|
||||
const registerPendingUser = (userData: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
registeredInstitution?: string
|
||||
const registerPendingUser = (userData: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
registeredInstitution?: string
|
||||
}) => {
|
||||
const newUser: User = {
|
||||
id: userData.id,
|
||||
|
|
@ -715,14 +753,25 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
setPendingUsers((prev) => [...prev, newUser]);
|
||||
};
|
||||
|
||||
const approveUser = (userId: string) => {
|
||||
setPendingUsers((prev) =>
|
||||
prev.map((user) =>
|
||||
user.id === userId
|
||||
? { ...user, approvalStatus: UserApprovalStatus.APPROVED }
|
||||
: user
|
||||
)
|
||||
);
|
||||
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) =>
|
||||
prev.map((user) =>
|
||||
user.id === userId
|
||||
? { ...user, approvalStatus: UserApprovalStatus.APPROVED }
|
||||
: user
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectUser = (userId: string) => {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,11 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
Usuários de Demonstração (Clique para preencher)
|
||||
</p>
|
||||
<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
|
||||
key={user.id}
|
||||
onClick={() => fillCredentials(user.email)}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|||
cidade: professionalData.cidade,
|
||||
conta_pix: professionalData.contaPix,
|
||||
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?
|
||||
extra_por_equipamento: false, // Default
|
||||
funcao_profissional_id: professionalData.funcaoId,
|
||||
|
|
@ -85,8 +85,8 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|||
className="min-h-screen flex items-center justify-center"
|
||||
style={{ backgroundColor: "#B9CF33" }}
|
||||
>
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 text-center">
|
||||
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<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-yellow-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
|
|
@ -97,23 +97,30 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Cadastro Realizado!
|
||||
Cadastro Pendente de Aprovação
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Seu cadastro de profissional foi realizado com sucesso e está
|
||||
aguardando aprovação.
|
||||
<p className="text-gray-600 mb-4">
|
||||
Seu cadastro foi realizado com sucesso e está aguardando aprovação.
|
||||
</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
|
||||
onClick={() => onNavigate("entrar")}
|
||||
className="w-full px-6 py-3 text-white font-semibold rounded-lg transition-colors"
|
||||
onClick={() => {
|
||||
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" }}
|
||||
>
|
||||
Ir para Login
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -122,20 +122,12 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsPending(false);
|
||||
setFormData({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
empresaId: "",
|
||||
});
|
||||
setAgreedToTerms(false);
|
||||
window.location.href = "/";
|
||||
}}
|
||||
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>
|
||||
</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">
|
||||
Empresa *
|
||||
</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 ? (
|
||||
<p className="text-xs sm:text-sm text-gray-500">
|
||||
Carregando empresas...
|
||||
|
|
@ -245,11 +242,19 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
style={{ focusRing: "2px solid #B9CF33" }}
|
||||
>
|
||||
<option value="">Selecione uma empresa</option>
|
||||
{companies.map((company) => (
|
||||
<option key={company.id} value={company.id}>
|
||||
{company.nome}
|
||||
</option>
|
||||
))}
|
||||
{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}>
|
||||
{company.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -274,7 +279,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
}
|
||||
error={
|
||||
error &&
|
||||
(error.includes("senha") || error.includes("coincidem"))
|
||||
(error.includes("senha") || error.includes("coincidem"))
|
||||
? error
|
||||
: undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,3 +194,71 @@ export async function getUniversities(): Promise<
|
|||
> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue