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
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,11 +123,10 @@ 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"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{link.name}
|
{link.name}
|
||||||
</button>
|
</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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
@ -182,50 +190,50 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||||
user.role === UserRole.EVENT_OWNER) && (
|
user.role === UserRole.EVENT_OWNER) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditProfileModalOpen(true);
|
setIsEditProfileModalOpen(true);
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
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">
|
<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]" />
|
<User size={20} className="text-[#492E61]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
Editar Perfil
|
Editar Perfil
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Atualize suas informações
|
Atualize suas informações
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Configurações - Apenas para CEO e Business Owner */}
|
{/* Configurações - Apenas para CEO e Business Owner */}
|
||||||
{(user.role === UserRole.BUSINESS_OWNER ||
|
{(user.role === UserRole.BUSINESS_OWNER ||
|
||||||
user.role === UserRole.SUPERADMIN) && (
|
user.role === UserRole.SUPERADMIN) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNavigate("configuracoes");
|
onNavigate("configuracoes");
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
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">
|
<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" />
|
<Settings size={20} className="text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
Configurações
|
Configurações
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Preferências da conta
|
Preferências da conta
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sair */}
|
{/* Sair */}
|
||||||
<button
|
<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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
@ -363,50 +371,50 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
{/* Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||||
user.role === UserRole.EVENT_OWNER) && (
|
user.role === UserRole.EVENT_OWNER) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditProfileModalOpen(true);
|
setIsEditProfileModalOpen(true);
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
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">
|
<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]" />
|
<User size={20} className="text-[#492E61]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
Editar Perfil
|
Editar Perfil
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Atualize suas informações
|
Atualize suas informações
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Configurações - Apenas para CEO e Business Owner */}
|
{/* Configurações - Apenas para CEO e Business Owner */}
|
||||||
{(user.role === UserRole.BUSINESS_OWNER ||
|
{(user.role === UserRole.BUSINESS_OWNER ||
|
||||||
user.role === UserRole.SUPERADMIN) && (
|
user.role === UserRole.SUPERADMIN) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNavigate("configuracoes");
|
onNavigate("configuracoes");
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white transition-colors text-left group"
|
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">
|
<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" />
|
<Settings size={20} className="text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
Configurações
|
Configurações
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Preferências da conta
|
Preferências da conta
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sair */}
|
{/* Sair */}
|
||||||
<button
|
<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 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}
|
||||||
/>
|
/>
|
||||||
|
|
@ -546,26 +554,26 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{/* Botão Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
{/* Botão Editar Perfil - Apenas para Fotógrafos e Clientes */}
|
||||||
{(user.role === UserRole.PHOTOGRAPHER ||
|
{(user.role === UserRole.PHOTOGRAPHER ||
|
||||||
user.role === UserRole.EVENT_OWNER) && (
|
user.role === UserRole.EVENT_OWNER) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditProfileModalOpen(true);
|
setIsEditProfileModalOpen(true);
|
||||||
setIsMobileMenuOpen(false);
|
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"
|
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">
|
<div className="w-10 h-10 rounded-lg bg-[#492E61]/10 flex items-center justify-center">
|
||||||
<User size={20} className="text-[#492E61]" />
|
<User size={20} className="text-[#492E61]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
Editar Perfil
|
Editar Perfil
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Atualize suas informações
|
Atualize suas informações
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Botão Sair */}
|
{/* Botão Sair */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
};
|
};
|
||||||
|
|
@ -695,12 +733,12 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// Funções para gerenciar usuários pendentes
|
// Funções para gerenciar usuários pendentes
|
||||||
const registerPendingUser = (userData: {
|
const registerPendingUser = (userData: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
registeredInstitution?: string
|
registeredInstitution?: string
|
||||||
}) => {
|
}) => {
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
id: userData.id,
|
id: userData.id,
|
||||||
|
|
@ -715,14 +753,25 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
setPendingUsers((prev) => [...prev, newUser]);
|
setPendingUsers((prev) => [...prev, newUser]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveUser = (userId: string) => {
|
const approveUser = async (userId: string) => {
|
||||||
setPendingUsers((prev) =>
|
const token = localStorage.getItem('token');
|
||||||
prev.map((user) =>
|
if (token) {
|
||||||
user.id === userId
|
try {
|
||||||
? { ...user, approvalStatus: UserApprovalStatus.APPROVED }
|
await apiApproveUser(userId, token);
|
||||||
: user
|
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) => {
|
const rejectUser = (userId: string) => {
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,11 +242,19 @@ 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
|
||||||
<option key={company.id} value={company.id}>
|
.sort((a, b) => {
|
||||||
{company.nome}
|
const nameA = a.nome.toLowerCase();
|
||||||
</option>
|
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>
|
</select>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -274,7 +279,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
error={
|
error={
|
||||||
error &&
|
error &&
|
||||||
(error.includes("senha") || error.includes("coincidem"))
|
(error.includes("senha") || error.includes("coincidem"))
|
||||||
? error
|
? error
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue