Merge pull request #23 from rede5/Front-back-integracao-task5

implementa correções importantes no fluxo de autenticação para garantir que usuários pendentes de aprovação não tenham acesso ao sistema, além de melhorias na interface administrativa.

Principais Alterações:

Autenticação e Segurança:
Login de Inativos: Adicionada verificação do campo ativo no login e na restauração de sessão (
AuthContext
). Usuários pendentes são impedidos de logar ou deslogados automaticamente.
Auto-login no Registro: O sistema agora impede o login automático após o cadastro se o usuário necessitar de aprovação.
Logout: Atualizado para chamar a rota /auth/logout na API, garantindo a invalidação dos cookies de sessão (HttpOnly).
Interface de Aprovação (Admin):
Dados Adicionais: Adicionada a coluna Telefone nas listagens de Empresas e Profissionais para facilitar a verificação.
Correção de Interface: Refatoração do componente 
UserApproval.tsx
 para corrigir um glitch visual que ocorria ao alternar entre as abas, onde as colunas ficavam desalinhadas ou duplicadas.
Como testar:

Tente fazer login com um usuário que ainda não foi aprovado (deve receber erro ou ser impedido).
Acesse a tela de "Aprovação de Cadastros" e verifique se o telefone aparece na lista.
Alterne entre as abas "Empresas" e "Profissionais" para garantir que a tabela renderiza corretamente.
This commit is contained in:
Andre F. Rodrigues 2025-12-15 15:08:44 -03:00 committed by GitHub
commit 85e07c2f26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 637 additions and 405 deletions

View file

@ -136,15 +136,7 @@ func main() {
api := r.Group("/api")
api.Use(auth.AuthMiddleware(cfg))
{
api.GET("/me", func(c *gin.Context) {
userID, _ := c.Get("userID")
role, _ := c.Get("role")
c.JSON(200, gin.H{
"user_id": userID,
"role": role,
"message": "You are authenticated",
})
})
api.GET("/me", authHandler.Me)
profGroup := api.Group("/profissionais")
{

View file

@ -1294,6 +1294,29 @@ const docTemplate = `{
}
}
},
"/api/me": {
"get": {
"description": "Get current authenticated user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/auth.loginResponse"
}
}
}
}
},
"/api/profissionais": {
"get": {
"security": [
@ -2209,6 +2232,10 @@ const docTemplate = `{
"email": {
"type": "string"
},
"empresa_id": {
"description": "Optional, for EVENT_OWNER",
"type": "string"
},
"nome": {
"type": "string"
},
@ -2242,12 +2269,21 @@ const docTemplate = `{
"ativo": {
"type": "boolean"
},
"company_name": {
"type": "string"
},
"email": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
},
"role": {
"type": "string"
}

View file

@ -1288,6 +1288,29 @@
}
}
},
"/api/me": {
"get": {
"description": "Get current authenticated user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/auth.loginResponse"
}
}
}
}
},
"/api/profissionais": {
"get": {
"security": [
@ -2203,6 +2226,10 @@
"email": {
"type": "string"
},
"empresa_id": {
"description": "Optional, for EVENT_OWNER",
"type": "string"
},
"nome": {
"type": "string"
},
@ -2236,12 +2263,21 @@
"ativo": {
"type": "boolean"
},
"company_name": {
"type": "string"
},
"email": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
},
"role": {
"type": "string"
}

View file

@ -42,6 +42,9 @@ definitions:
properties:
email:
type: string
empresa_id:
description: Optional, for EVENT_OWNER
type: string
nome:
type: string
role:
@ -69,10 +72,16 @@ definitions:
properties:
ativo:
type: boolean
company_name:
type: string
email:
type: string
id:
type: string
name:
type: string
phone:
type: string
role:
type: string
type: object
@ -1193,6 +1202,21 @@ paths:
summary: Update function
tags:
- funcoes
/api/me:
get:
consumes:
- application/json
description: Get current authenticated user
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/auth.loginResponse'
summary: Get current user
tags:
- auth
/api/profissionais:
get:
consumes:

View file

@ -19,11 +19,12 @@ func NewHandler(service *Service) *Handler {
}
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
Nome string `json:"nome" binding:"required"`
Telefone string `json:"telefone"`
Role string `json:"role" binding:"required"` // Role is now required
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
Nome string `json:"nome" binding:"required"`
Telefone string `json:"telefone"`
Role string `json:"role" binding:"required"` // Role is now required
EmpresaID string `json:"empresa_id"` // Optional, for EVENT_OWNER
}
// Register godoc
@ -46,15 +47,27 @@ func (h *Handler) Register(c *gin.Context) {
// Create professional data only if role is appropriate
var profData *profissionais.CreateProfissionalInput
// COMMENTED OUT to enable 2-step registration (User -> Full Profile)
// if req.Role == RolePhotographer || req.Role == RoleBusinessOwner {
// profData = &profissionais.CreateProfissionalInput{
// Nome: req.Nome,
// Whatsapp: &req.Telefone,
// }
// }
// For PHOTOGRAPHER or BUSINESS_OWNER, we might populate this if we were doing 1-step,
// but actually 'nome' and 'telefone' are passed as args now.
// We keep passing nil for profData because Service logic for Professionals relies on 'CreateProfissionalInput'
// However, I updated Service to take nome/telefone directly.
// Wait, the Service code I JUST wrote takes (email, senha, role, nome, telefone, empresaID, profissionalData).
// If role is Photographer, the Service code checks `profissionalData`.
// I should probably populate `profissionalData` if it's a professional.
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, profData)
if req.Role == "PHOTOGRAPHER" || req.Role == "BUSINESS_OWNER" {
profData = &profissionais.CreateProfissionalInput{
Nome: req.Nome,
Whatsapp: &req.Telefone,
}
}
var empresaIDPtr *string
if req.EmpresaID != "" {
empresaIDPtr = &req.EmpresaID
}
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, empresaIDPtr, profData)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
@ -117,10 +130,13 @@ type loginResponse struct {
}
type userResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
Name string `json:"name,omitempty"`
Phone string `json:"phone,omitempty"`
CompanyName string `json:"company_name,omitempty"`
}
// Login godoc
@ -258,6 +274,52 @@ func (h *Handler) Logout(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}
// Me godoc
// @Summary Get current user
// @Description Get current authenticated user
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} loginResponse
// @Router /api/me [get]
func (h *Handler) Me(c *gin.Context) {
// User ID comes from context (AuthMiddleware)
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
// We can fetch fresh user data
user, err := h.service.GetUser(c.Request.Context(), userID.(string))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
var empresaNome string
if user.EmpresaNome.Valid {
empresaNome = user.EmpresaNome.String
}
resp := loginResponse{
User: userResponse{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Role: user.Role,
Ativo: user.Ativo,
Name: user.Nome,
Phone: user.Whatsapp,
CompanyName: empresaNome,
},
}
// Note: We are not returning AccessToken/ExpiresAt here as they are already set/active.
// But to match loginResponse structure we can leave them empty or fill appropriately if we were refreshing.
// For session restore, we mainly need the User object.
c.JSON(http.StatusOK, resp)
}
// ListPending godoc
// @Summary List pending users
// @Description List users with ativo=false
@ -287,23 +349,23 @@ func (h *Handler) ListPending(c *gin.Context) {
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
nome := u.Nome
whatsapp := u.Whatsapp
var empresaNome string
if u.EmpresaNome.Valid {
empresaNome = u.EmpresaNome.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,
"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,
"company_name": empresaNome,
}
}
@ -503,12 +565,20 @@ func (h *Handler) GetUser(c *gin.Context) {
return
}
var empresaNome string
if user.EmpresaNome.Valid {
empresaNome = user.EmpresaNome.String
}
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,
"id": uuid.UUID(user.ID.Bytes).String(),
"email": user.Email,
"role": user.Role,
"ativo": user.Ativo,
"created_at": user.CriadoEm.Time,
"name": user.Nome,
"phone": user.Whatsapp,
"company_name": empresaNome,
}
c.JSON(http.StatusOK, resp)

View file

@ -43,7 +43,7 @@ func NewService(queries *generated.Queries, profissionaisService *profissionais.
}
}
func (s *Service) Register(ctx context.Context, email, senha, role string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) {
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) {
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
if err != nil {
@ -71,6 +71,30 @@ func (s *Service) Register(ctx context.Context, email, senha, role string, profi
}
}
// If role is 'EVENT_OWNER', create client profile
if role == RoleEventOwner {
userID := user.ID
var empID pgtype.UUID
if empresaID != nil && *empresaID != "" {
parsedEmpID, err := uuid.Parse(*empresaID)
if err == nil {
empID.Bytes = parsedEmpID
empID.Valid = true
}
}
_, err := s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
UsuarioID: userID,
EmpresaID: empID,
Nome: pgtype.Text{String: nome, Valid: nome != ""},
Telefone: pgtype.Text{String: telefone, Valid: telefone != ""},
})
if err != nil {
_ = s.queries.DeleteUsuario(ctx, user.ID)
return nil, err
}
}
return &user, nil
}
@ -237,6 +261,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error {
{"admin@photum.com", RoleSuperAdmin, "Dev Admin"},
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"},
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"},
{"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE"},
}
for _, u := range demoUsers {
@ -270,7 +295,7 @@ func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow
return s.queries.ListAllUsuarios(ctx)
}
func (s *Service) GetUser(ctx context.Context, id string) (*generated.Usuario, error) {
func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) {
parsedUUID, err := uuid.Parse(id)
if err != nil {
return nil, err

View file

@ -14,6 +14,16 @@ type AnosFormatura struct {
CriadoEm pgtype.Timestamptz `json:"criado_em"`
}
type CadastroCliente struct {
ID pgtype.UUID `json:"id"`
UsuarioID pgtype.UUID `json:"usuario_id"`
EmpresaID pgtype.UUID `json:"empresa_id"`
Nome pgtype.Text `json:"nome"`
Telefone pgtype.Text `json:"telefone"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
}
type CadastroFot struct {
ID pgtype.UUID `json:"id"`
Fot int32 `json:"fot"`

View file

@ -11,6 +11,39 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const createCadastroCliente = `-- name: CreateCadastroCliente :one
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
VALUES ($1, $2, $3, $4)
RETURNING id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em
`
type CreateCadastroClienteParams struct {
UsuarioID pgtype.UUID `json:"usuario_id"`
EmpresaID pgtype.UUID `json:"empresa_id"`
Nome pgtype.Text `json:"nome"`
Telefone pgtype.Text `json:"telefone"`
}
func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroClienteParams) (CadastroCliente, error) {
row := q.db.QueryRow(ctx, createCadastroCliente,
arg.UsuarioID,
arg.EmpresaID,
arg.Nome,
arg.Telefone,
)
var i CadastroCliente
err := row.Scan(
&i.ID,
&i.UsuarioID,
&i.EmpresaID,
&i.Nome,
&i.Telefone,
&i.CriadoEm,
&i.AtualizadoEm,
)
return i, err
}
const createUsuario = `-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role, ativo)
VALUES ($1, $2, $3, false)
@ -69,13 +102,33 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario,
}
const getUsuarioByID = `-- name: GetUsuarioByID :one
SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios
WHERE id = $1 LIMIT 1
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.nome as empresa_nome
FROM usuarios u
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
LEFT JOIN empresas e ON cc.empresa_id = e.id
WHERE u.id = $1 LIMIT 1
`
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, error) {
type GetUsuarioByIDRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
}
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuarioByIDRow, error) {
row := q.db.QueryRow(ctx, getUsuarioByID, id)
var i Usuario
var i GetUsuarioByIDRow
err := row.Scan(
&i.ID,
&i.Email,
@ -84,6 +137,9 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
&i.Nome,
&i.Whatsapp,
&i.EmpresaNome,
)
return i, err
}
@ -132,21 +188,26 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er
const listUsuariosPending = `-- name: ListUsuariosPending :many
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
cp.nome, cp.whatsapp
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.nome as empresa_nome
FROM usuarios u
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
LEFT JOIN empresas e ON cc.empresa_id = e.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"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
}
func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) {
@ -166,6 +227,7 @@ func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendin
&i.CriadoEm,
&i.Nome,
&i.Whatsapp,
&i.EmpresaNome,
); err != nil {
return nil, err
}

View file

@ -8,8 +8,15 @@ SELECT * FROM usuarios
WHERE email = $1 LIMIT 1;
-- name: GetUsuarioByID :one
SELECT * FROM usuarios
WHERE id = $1 LIMIT 1;
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.nome as empresa_nome
FROM usuarios u
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
LEFT JOIN empresas e ON cc.empresa_id = e.id
WHERE u.id = $1 LIMIT 1;
-- name: DeleteUsuario :exec
DELETE FROM usuarios
@ -17,9 +24,13 @@ WHERE id = $1;
-- name: ListUsuariosPending :many
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
cp.nome, cp.whatsapp
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.nome as empresa_nome
FROM usuarios u
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
LEFT JOIN empresas e ON cc.empresa_id = e.id
WHERE u.ativo = false
ORDER BY u.criado_em DESC;
@ -38,3 +49,8 @@ RETURNING *;
SELECT id, email, role, ativo, criado_em, atualizado_em
FROM usuarios
ORDER BY criado_em DESC;
-- name: CreateCadastroCliente :one
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
VALUES ($1, $2, $3, $4)
RETURNING *;

View file

@ -301,3 +301,15 @@ BEGIN
ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor;
END $$;
-- Cadastro Clientes (Representatives of Companies)
CREATE TABLE IF NOT EXISTS cadastro_clientes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
empresa_id UUID REFERENCES empresas(id) ON DELETE SET NULL,
nome VARCHAR(255), -- Name of the representative
telefone VARCHAR(20),
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(usuario_id)
);

View file

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import { User, UserRole } from '../types';
// Mock Users Database
@ -38,14 +38,58 @@ interface AuthContextType {
user: User | null;
login: (email: string, password?: string) => Promise<boolean>;
logout: () => void;
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
availableUsers: User[]; // Helper for the login screen demo
token: string | null;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(localStorage.getItem("token"));
useEffect(() => {
const restoreSession = async () => {
if (!token) return;
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const backendUser = data.user;
const mappedUser: User = {
id: backendUser.id,
email: backendUser.email,
name: backendUser.email.split('@')[0],
role: backendUser.role as UserRole,
ativo: backendUser.ativo,
};
if (!backendUser.ativo) {
console.warn("User is not active, logging out.");
logout();
return;
}
setUser(mappedUser);
} else {
// Token invalid or expired
logout();
}
} catch (err) {
console.error("Session restore error:", err);
logout();
}
};
if (!user && token) {
restoreSession();
}
}, [token]); // removed 'user' from dependency to avoid loop if user is set, though !user check handles it. safer to just depend on token mount.
const getErrorMessage = (errorKey: string): string => {
// Map backend error messages to Portuguese
@ -93,9 +137,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const data = await response.json();
// Store token (optional, if you need it for other requests outside cookies)
localStorage.setItem('token', data.access_token);
const backendUser = data.user;
// Enforce active check
@ -103,6 +144,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
throw new Error("Cadastro pendente de aprovação.");
}
// Store token (optional, if you need it for other requests outside cookies)
localStorage.setItem('token', data.access_token);
setToken(data.access_token);
// Map backend user to frontend User type
const mappedUser: User = {
id: backendUser.id,
@ -121,17 +166,41 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
const logout = async () => {
try {
if (token) {
await fetch(`${import.meta.env.VITE_API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Though refresh token is in cookie
},
// body: JSON.stringify({ refresh_token: ... }) // If we had it in client state, but it is in cookie.
});
}
} catch (error) {
console.error("Logout failed", error);
} finally {
localStorage.removeItem('token');
setToken(null);
setUser(null);
// Force redirect or let app router handle it safely
window.location.href = '/';
}
};
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => {
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => {
try {
// Destructure to separate empresaId from the rest
const { empresaId, ...rest } = data;
const payload = {
...rest,
empresa_id: empresaId
};
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(payload)
});
if (!response.ok) {
@ -142,14 +211,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const responseData = await response.json();
if (responseData.access_token) {
localStorage.setItem('token', responseData.access_token);
}
// IF user is returned (auto-login), logic:
// Only set user if they are ACTIVE (which they won't be for standard clients)
// Only set user/token if they are ACTIVE (which they won't be for standard clients/professionals)
// This allows the "Pending Approval" modal to show instead of auto-redirecting.
if (responseData.user && responseData.user.ativo) {
if (responseData.access_token) {
localStorage.setItem('token', responseData.access_token);
setToken(responseData.access_token);
}
const backendUser = responseData.user;
const mappedUser: User = {
id: backendUser.id,
@ -160,6 +230,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
};
setUser(mappedUser);
}
// If user is NOT active, we do NOT set the token/user state, preventing auto-login.
return {
success: true,
@ -173,7 +244,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
};
return (
<AuthContext.Provider value={{ user, login, logout, register, availableUsers: MOCK_USERS }}>
<AuthContext.Provider value={{ user, token, login, logout, register, availableUsers: MOCK_USERS }}>
{children}
</AuthContext.Provider>
);

View file

@ -201,6 +201,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
{ 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 },
{ id: "4", name: "CLIENTE TESTE", email: "cliente@photum.com", role: UserRole.EVENT_OWNER },
].map((user) => (
<button
key={user.id}

View file

@ -76,6 +76,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
senha: formData.password,
telefone: formData.phone,
role: "EVENT_OWNER", // Client Role
empresaId: formData.empresaId,
});
setIsLoading(false);
setIsPending(true);

View file

@ -1,5 +1,10 @@
import React, { useState } from "react";
import { useData } from "../contexts/DataContext";
import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import {
getPendingUsers,
approveUser as apiApproveUser,
rejectUser as apiRejectUser,
} from "../services/apiService";
import { UserApprovalStatus } from "../types";
import {
CheckCircle,
@ -17,62 +22,111 @@ interface UserApprovalProps {
}
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
const { pendingUsers, approveUser, rejectUser } = useData();
const { token } = useAuth();
const [users, setUsers] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>(
"ALL"
);
const [activeTab, setActiveTab] = useState<"normal" | "professional">(
"normal"
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
"cliente"
);
const [isProcessing, setIsProcessing] = useState<string | null>(null);
const fetchUsers = async () => {
if (!token) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const result = await getPendingUsers(token);
if (result.data) {
// Mapear dados do backend para o formato esperado pelo componente, se necessário
// Supondo que o backend retorna estrutura compatível ou fazemos o map aqui
const mappedUsers = result.data.map((u: any) => ({
...u,
approvalStatus: u.ativo
? UserApprovalStatus.APPROVED
: UserApprovalStatus.PENDING, // Simplificação, backend deve retornar status real se houver rejected
// Se o backend não retornar status explícito, assumimos pendente se !ativo
// Mas idealmente o backend retornaria um status enum
}));
setUsers(mappedUsers);
}
} catch (error) {
console.error("Erro ao buscar usuários:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, [token]);
const handleApprove = async (userId: string) => {
if (!token) return;
setIsProcessing(userId);
// Simular processamento
setTimeout(() => {
approveUser(userId);
try {
await apiApproveUser(userId, token);
// Atualizar lista após aprovação
await fetchUsers();
} catch (error) {
console.error("Erro ao aprovar usuário:", error);
alert("Erro ao aprovar usuário");
} finally {
setIsProcessing(null);
}, 800);
}
};
const handleReject = async (userId: string) => {
if (!token) return;
setIsProcessing(userId);
// Simular processamento
setTimeout(() => {
rejectUser(userId);
try {
await apiRejectUser(userId, token);
await fetchUsers();
} catch (error) {
console.error("Erro ao rejeitar usuário:", error);
alert("Erro ao rejeitar usuário");
} finally {
setIsProcessing(null);
}, 800);
}
};
// Separar usuários normais e profissionais (profissionais têm role PHOTOGRAPHER)
const normalUsers = pendingUsers.filter(
(user) => user.role !== "PHOTOGRAPHER"
// Separar usuários Clientes (EVENT_OWNER) e Profissionais (PHOTOGRAPHER)
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN
const clientUsers = users.filter(
(user) => user.role === "EVENT_OWNER"
);
const professionalUsers = pendingUsers.filter(
const professionalUsers = users.filter(
(user) => user.role === "PHOTOGRAPHER"
);
// Filtrar usuários baseado na aba ativa
const currentUsers = activeTab === "normal" ? normalUsers : professionalUsers;
const currentUsers = activeTab === "cliente" ? clientUsers : professionalUsers;
const filteredUsers = currentUsers.filter((user) => {
const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.registeredInstitution
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ??
false);
(user.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.email || "").toLowerCase().includes(searchTerm.toLowerCase());
// Remover filtro por registeredInstitution se não vier do backend ainda
const matchesStatus =
statusFilter === "ALL" || user.approvalStatus === statusFilter;
// Por enquanto, como o backend retorna apenas pendentes na rota /pending (conforme nome da rota),
// o statusFilter pode ser redundante se a rota só traz pendentes.
// Mas se o backend trouxer todos, o filtro funciona.
// Se a rota for /users/pending, assumimos que todos são pendentes.
// VAMOS ASSUMIR QUE O BACKEND SÓ RETORNA PENDENTES POR ENQUANTO.
// Mas para manter a UI, vamos considerar todos como Pendentes se status não vier.
return matchesSearch && matchesStatus;
return matchesSearch;
});
const getStatusBadge = (status: UserApprovalStatus) => {
switch (status) {
// Se status undefined, assume pendente para visualização
const s = status || UserApprovalStatus.PENDING;
switch (s) {
case UserApprovalStatus.PENDING:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
@ -94,19 +148,11 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
Rejeitado
</span>
);
default:
return null;
}
};
const pendingCount = currentUsers.filter(
(u) => u.approvalStatus === UserApprovalStatus.PENDING
).length;
const approvedCount = currentUsers.filter(
(u) => u.approvalStatus === UserApprovalStatus.APPROVED
).length;
const rejectedCount = currentUsers.filter(
(u) => u.approvalStatus === UserApprovalStatus.REJECTED
).length;
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@ -124,41 +170,37 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
<div className="mb-6 border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab("normal")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${
activeTab === "normal"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
onClick={() => setActiveTab("cliente")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "cliente"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<Users className="w-5 h-5" />
Cadastros Normais
Cadastros Empresas
<span
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${
activeTab === "normal"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "cliente"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{normalUsers.length}
{clientUsers.length}
</span>
</button>
<button
onClick={() => setActiveTab("professional")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${
activeTab === "professional"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
onClick={() => setActiveTab("profissional")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "profissional"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<Briefcase className="w-5 h-5" />
Cadastros Profissionais
<span
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${
activeTab === "professional"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "profissional"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{professionalUsers.length}
</span>
@ -166,51 +208,6 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
</nav>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendentes</p>
<p className="text-3xl font-bold text-yellow-600">
{pendingCount}
</p>
</div>
<div className="p-3 bg-yellow-100 rounded-full">
<Clock className="w-8 h-8 text-yellow-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Aprovados</p>
<p className="text-3xl font-bold text-green-600">
{approvedCount}
</p>
</div>
<div className="p-3 bg-green-100 rounded-full">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Rejeitados</p>
<p className="text-3xl font-bold text-red-600">
{rejectedCount}
</p>
</div>
<div className="p-3 bg-red-100 rounded-full">
<XCircle className="w-8 h-8 text-red-600" />
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex flex-col sm:flex-row gap-4">
@ -219,183 +216,40 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Buscar por nome, email ou universidade..."
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
/>
</div>
{/* Status Filter */}
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
>
<option value="ALL">Todos os Status</option>
<option value={UserApprovalStatus.PENDING}>Pendentes</option>
<option value={UserApprovalStatus.APPROVED}>Aprovados</option>
<option value={UserApprovalStatus.REJECTED}>Rejeitados</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
{activeTab === "normal" ? (
// Tabela de Cadastros Normais
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nome
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Telefone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Empresa
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data de Cadastro
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.length === 0 ? (
<tr>
<td
colSpan={7}
className="px-6 py-12 text-center text-gray-500"
>
<div className="flex flex-col items-center justify-center">
<Clock className="w-12 h-12 text-gray-300 mb-3" />
<p className="text-lg font-medium">
Nenhum cadastro encontrado
</p>
<p className="text-sm">
Não cadastros que correspondam aos filtros
selecionados.
</p>
</div>
</td>
</tr>
) : (
filteredUsers.map((user) => (
<tr
key={user.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{user.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.email}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.phone || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.registeredInstitution || (
<span className="text-gray-400 italic">
Não cadastrado
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.createdAt
? new Date(user.createdAt).toLocaleDateString(
"pt-BR"
)
: "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(user.approvalStatus!)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{user.approvalStatus ===
UserApprovalStatus.PENDING && (
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-green-600 hover:bg-green-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprovar
</Button>
<Button
size="sm"
onClick={() => handleReject(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-red-600 hover:bg-red-700 text-white"
>
<XCircle className="w-4 h-4 mr-1" />
Rejeitar
</Button>
</div>
)}
{user.approvalStatus ===
UserApprovalStatus.APPROVED && (
<span className="text-green-600 text-xs">
Aprovado
</span>
)}
{user.approvalStatus ===
UserApprovalStatus.REJECTED && (
<span className="text-red-600 text-xs">
Rejeitado
</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
{isLoading ? (
<div className="p-8 text-center text-gray-500">
Carregando solicitações...
</div>
) : (
// Tabela de Cadastros Profissionais
<table className="min-w-full divide-y divide-gray-200">
<table key={activeTab} className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nome
</th>
{activeTab === "cliente" && (
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Empresa
</th>
)}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Telefone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Função Profissional
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data de Cadastro
</th>
@ -411,32 +265,41 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
{filteredUsers.length === 0 ? (
<tr>
<td
colSpan={7}
colSpan={activeTab === "cliente" ? 7 : 6}
className="px-6 py-12 text-center text-gray-500"
>
<div className="flex flex-col items-center justify-center">
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
{activeTab === "cliente" ? (
<Users className="w-12 h-12 text-gray-300 mb-3" />
) : (
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
)}
<p className="text-lg font-medium">
Nenhum cadastro profissional encontrado
</p>
<p className="text-sm">
Não cadastros profissionais que correspondam aos
filtros selecionados.
{activeTab === "cliente"
? "Nenhum cadastro de empresa encontrado"
: "Nenhum cadastro profissional encontrado"}
</p>
</div>
</td>
</tr>
) : (
filteredUsers.map((user) => (
filteredUsers.map((user, index) => (
<tr
key={user.id}
key={`${user.id}-${index}`}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{user.name}
{user.name || user.email}
</div>
</td>
{activeTab === "cliente" && (
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.company_name || "-"}
</div>
</td>
)}
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.email}
@ -449,73 +312,52 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{(user as any).funcao || (
<span className="text-gray-400 italic">
Não cadastrado
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.createdAt
? new Date(user.createdAt).toLocaleDateString(
"pt-BR"
)
{user.created_at
? new Date(user.created_at).toLocaleDateString(
"pt-BR"
)
: "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(user.approvalStatus!)}
{getStatusBadge(
user.approvalStatus || UserApprovalStatus.PENDING
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{user.approvalStatus ===
UserApprovalStatus.PENDING && (
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-green-600 hover:bg-green-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprovar
</Button>
<Button
size="sm"
onClick={() => handleReject(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-red-600 hover:bg-red-700 text-white"
>
<XCircle className="w-4 h-4 mr-1" />
Rejeitar
</Button>
</div>
)}
{user.approvalStatus ===
UserApprovalStatus.APPROVED && (
<span className="text-green-600 text-xs">
Aprovado
</span>
)}
{user.approvalStatus ===
UserApprovalStatus.REJECTED && (
<span className="text-red-600 text-xs">
Rejeitado
</span>
)}
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-green-600 hover:bg-green-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprovar
</Button>
<Button
size="sm"
onClick={() => handleReject(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-red-600 hover:bg-red-700 text-white"
>
<XCircle className="w-4 h-4 mr-1" />
Rejeitar
</Button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
</div>
</div>
</div>
)
}
</div >
</div >
</div >
</div >
);
};

View file

@ -262,3 +262,37 @@ export async function approveUser(userId: string, token: string): Promise<ApiRes
};
}
}
/**
* Rejeita um usuário
*/
export async function rejectUser(userId: string, token: string): Promise<ApiResponse<any>> {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/reject`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
// body: JSON.stringify({ reason }) // Future improvement
});
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 rejecting user:", error);
return {
data: null,
error: error instanceof Error ? error.message : "Erro desconhecido",
isBackendDown: true,
};
}
}