diff --git a/backend/internal/agenda/handler.go b/backend/internal/agenda/handler.go index 7bfe242..682aa9a 100644 --- a/backend/internal/agenda/handler.go +++ b/backend/internal/agenda/handler.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" ) type Handler struct { @@ -35,6 +36,12 @@ func (h *Handler) Create(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + userIDStr := c.GetString("userID") userID, err := uuid.Parse(userIDStr) if err != nil { @@ -128,6 +135,12 @@ func (h *Handler) Update(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + fmt.Printf("Update Payload: %+v\n", req) agenda, err := h.service.Update(c.Request.Context(), id, req) @@ -188,6 +201,17 @@ func (h *Handler) AssignProfessional(c *gin.Context) { return } + if idParam == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"}) + return + } + + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + profID, err := uuid.Parse(req.ProfessionalID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de profissional inválido"}) @@ -228,6 +252,12 @@ func (h *Handler) RemoveProfessional(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + if err := h.service.RemoveProfessional(c.Request.Context(), agendaID, profID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao remover profissional: " + err.Error()}) return @@ -240,6 +270,7 @@ func (h *Handler) RemoveProfessional(c *gin.Context) { // @Summary Get professionals assigned to agenda // @Tags agenda // @Router /api/agenda/{id}/professionals [get] + func (h *Handler) GetProfessionals(c *gin.Context) { idParam := c.Param("id") agendaID, err := uuid.Parse(idParam) @@ -254,6 +285,34 @@ func (h *Handler) GetProfessionals(c *gin.Context) { return } + // Security: Mask data for RESEARCHER + if c.GetString("role") == "RESEARCHER" { + for i := range profs { + // Set sensitive fields to generic or empty values + // Using generated struct pointers or values? + // generated.GetAgendaProfessionalsRow has fields like Email (pgtype.Text) + // We can override them. + // Note: 'profs' is a slice of structs, we iterate by index to modify. + + // Mask Email + profs[i].Email = pgtype.Text{String: "bloqueado@acesso.restrito", Valid: true} + + // Mask Phone/Whatsapp if available in struct + // Checking struct definition... list includes p.* + // p (CadastroProfissionais) has Whatsapp, CpfCnpjTitular, Banco, Agencia, ContaPix + + profs[i].Whatsapp = pgtype.Text{String: "Bloqueado", Valid: true} + + profs[i].CpfCnpjTitular = pgtype.Text{String: "***", Valid: true} + + profs[i].Banco = pgtype.Text{String: "***", Valid: true} + + profs[i].Agencia = pgtype.Text{String: "***", Valid: true} + + profs[i].ContaPix = pgtype.Text{String: "***", Valid: true} + } + } + c.JSON(http.StatusOK, profs) } @@ -444,6 +503,12 @@ func (h *Handler) NotifyLogistics(c *gin.Context) { } _ = c.ShouldBindJSON(&req) + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + go h.service.NotifyLogistics(context.Background(), agendaID, req.PassengerOrders) c.JSON(http.StatusOK, gin.H{"message": "Notificação de logística iniciada com sucesso."}) diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index d8e8e09..d220a32 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -419,14 +419,15 @@ func (h *Handler) ListPending(c *gin.Context) { } 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, - "company_name": empresaNome, + "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, + "professional_type": u.TipoProfissional.String, // Add this } } diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 6c3b0e8..6a4a4c5 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -22,6 +22,7 @@ const ( RolePhotographer = "PHOTOGRAPHER" RoleEventOwner = "EVENT_OWNER" RoleAgendaViewer = "AGENDA_VIEWER" + RoleResearcher = "RESEARCHER" ) type Service struct { @@ -269,6 +270,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error { {"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"}, {"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"}, {"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE"}, + {"pesquisa@photum.com", RoleResearcher, "PESQUISADOR"}, } for _, u := range demoUsers { diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 0431556..aa78d52 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -23,7 +23,8 @@ INSERT INTO funcoes_profissionais (nome) VALUES ('Cinegrafista'), ('Recepcionista'), ('Fixo Photum'), -('Controle') +('Controle'), +('Pesquisa') ON CONFLICT (nome) DO NOTHING; CREATE TABLE IF NOT EXISTS cadastro_profissionais ( diff --git a/backend/internal/escalas/handler.go b/backend/internal/escalas/handler.go index 5930ff3..3f1cb4b 100644 --- a/backend/internal/escalas/handler.go +++ b/backend/internal/escalas/handler.go @@ -31,6 +31,12 @@ func (h *Handler) Create(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + escala, err := h.service.Create(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -76,7 +82,11 @@ func (h *Handler) ListByAgenda(c *gin.Context) { } var phone string if e.Whatsapp.Valid { - phone = e.Whatsapp.String + if c.GetString("role") == "RESEARCHER" { + phone = "Bloqueado" + } else { + phone = e.Whatsapp.String + } } var profFuncao string if e.FuncaoNome.Valid { @@ -116,6 +126,12 @@ func (h *Handler) Delete(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + err := h.service.Delete(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -148,6 +164,12 @@ func (h *Handler) Update(c *gin.Context) { return } + // Security: Block RESEARCHER + if c.GetString("role") == "RESEARCHER" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"}) + return + } + _, err := h.service.Update(c.Request.Context(), id, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index f5a1a62..0356091 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -82,6 +82,10 @@ export const Navbar: React.FC = ({ onNavigate, currentPage }) => { return [ { name: "Agenda", path: "painel" }, ]; + case UserRole.RESEARCHER: + return [ + { name: "Painel", path: "painel" }, + ]; case UserRole.EVENT_OWNER: return [ { name: "Meus Eventos", path: "painel" }, @@ -104,6 +108,7 @@ export const Navbar: React.FC = ({ onNavigate, currentPage }) => { if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo"; if (user.role === UserRole.SUPERADMIN) return "Super Admin"; if (user.role === UserRole.AGENDA_VIEWER) return "Visualizador"; + if (user.role === UserRole.RESEARCHER) return "Pesquisa"; return ""; }; diff --git a/frontend/components/System/SystemSettings.tsx b/frontend/components/System/SystemSettings.tsx index c4f314c..8423c8b 100644 --- a/frontend/components/System/SystemSettings.tsx +++ b/frontend/components/System/SystemSettings.tsx @@ -1,16 +1,17 @@ import React, { useState } from "react"; import { SimpleCrud } from "./SimpleCrud"; import { PriceTableEditor } from "./PriceTableEditor"; -import { Building2, GraduationCap, Calendar, DollarSign, Database } from "lucide-react"; +import { Building2, GraduationCap, Calendar, DollarSign, Database, Briefcase } from "lucide-react"; export const SystemSettings: React.FC = () => { - const [activeTab, setActiveTab] = useState<"empresas" | "cursos" | "tipos_evento" | "anos_formatura" | "precos">("empresas"); + const [activeTab, setActiveTab] = useState<"empresas" | "cursos" | "tipos_evento" | "anos_formatura" | "precos" | "funcoes">("empresas"); const tabs = [ { id: "empresas", label: "Empresas", icon: Building2 }, { id: "cursos", label: "Cursos", icon: GraduationCap }, { id: "tipos_evento", label: "Tipos de Evento", icon: Calendar }, { id: "anos_formatura", label: "Anos de Formatura", icon: Database }, + { id: "funcoes", label: "Funções", icon: Briefcase }, { id: "precos", label: "Tabela de Preços", icon: DollarSign }, ]; @@ -68,6 +69,13 @@ export const SystemSettings: React.FC = () => { columns={[{ key: "ano_semestre", label: "Ano/Semestre (Ex: 2024.1)" }]} /> )} + {activeTab === "funcoes" && ( + + )} {activeTab === "precos" && ( )} diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index dcb8dbf..8cc0bbb 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -966,7 +966,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ }; const getEventsByRole = (userId: string, role: string) => { - if (role === "SUPERADMIN" || role === "BUSINESS_OWNER") { + if (role === "SUPERADMIN" || role === "BUSINESS_OWNER" || role === "RESEARCHER") { return events; } if (role === "EVENT_OWNER") { diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index 5e734ff..b90f508 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -532,7 +532,7 @@ export const Dashboard: React.FC = ({ }; const renderRoleSpecificActions = () => { - if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER) return null; + if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER || user.role === UserRole.RESEARCHER) return null; const label = user.role === UserRole.EVENT_OWNER diff --git a/frontend/pages/EventDetails.tsx b/frontend/pages/EventDetails.tsx index 6219399..dee4b78 100644 --- a/frontend/pages/EventDetails.tsx +++ b/frontend/pages/EventDetails.tsx @@ -23,7 +23,7 @@ const EventDetails: React.FC = () => { if (!event) return
Evento não encontrado.
; // Check if user can view logistics - const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER; + const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER && user?.role !== UserRole.RESEARCHER; // Use event.date which is already YYYY-MM-DD from DataContext const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString(); diff --git a/frontend/pages/Login.tsx b/frontend/pages/Login.tsx index 7f3ebd2..0d85878 100644 --- a/frontend/pages/Login.tsx +++ b/frontend/pages/Login.tsx @@ -117,6 +117,8 @@ export const Login: React.FC = ({ onNavigate }) => { return "Fotógrafo"; case UserRole.EVENT_OWNER: return "Cliente"; + case UserRole.RESEARCHER: + return "Pesquisador"; default: return role; } @@ -267,6 +269,7 @@ export const Login: React.FC = ({ onNavigate }) => { { 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 }, + { id: "5", name: "PESQUISADOR", email: "pesquisa@photum.com", role: UserRole.RESEARCHER }, ].map((user) => (