package handlers import ( "encoding/json" "net/http" "strconv" "github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/services" ) type AdminHandlers struct { adminService *services.AdminService auditService *services.AuditService jobService *services.JobService cloudflareService *services.CloudflareService notificationService *services.NotificationService } type RoleAccess struct { Role string `json:"role"` Description string `json:"description"` Actions []string `json:"actions"` } type UpdateCompanyStatusRequest struct { Active *bool `json:"active,omitempty"` Verified *bool `json:"verified,omitempty"` } type UpdateJobStatusRequest struct { Status string `json:"status"` } type CreateTagRequest struct { Name string `json:"name"` Category string `json:"category"` } type UpdateTagRequest struct { Name *string `json:"name,omitempty"` Active *bool `json:"active,omitempty"` } func NewAdminHandlers( adminService *services.AdminService, auditService *services.AuditService, jobService *services.JobService, cloudflareService *services.CloudflareService, notificationService *services.NotificationService, ) *AdminHandlers { return &AdminHandlers{ adminService: adminService, auditService: auditService, jobService: jobService, cloudflareService: cloudflareService, notificationService: notificationService, } } func (h *AdminHandlers) ListAccessRoles(w http.ResponseWriter, r *http.Request) { roles := []RoleAccess{ { Role: "admin", Description: "Administrador geral da plataforma", Actions: []string{ "criar/editar/excluir usuários", "aprovar empresas", "moderar vagas", "gerir tags e categorias", }, }, { Role: "moderador", Description: "Moderação de vagas e conteúdo", Actions: []string{ "aprovar, recusar ou pausar vagas", "marcar vagas denunciadas", "revisar empresas pendentes", }, }, { Role: "suporte", Description: "Suporte ao usuário", Actions: []string{ "acessar perfil de usuário", "resetar senhas", "analisar logs de acesso", }, }, { Role: "financeiro", Description: "Gestão financeira e faturamento", Actions: []string{ "ver planos e pagamentos", "exportar relatórios financeiros", "controlar notas e cobranças", }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(roles) } func (h *AdminHandlers) ListLoginAudits(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) entries, err := h.auditService.ListLogins(r.Context(), limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(entries) } func (h *AdminHandlers) ListCompanies(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 { limit = 10 } var verified *bool if verifiedParam := r.URL.Query().Get("verified"); verifiedParam != "" { value := verifiedParam == "true" verified = &value } companies, total, err := h.adminService.ListCompanies(r.Context(), verified, page, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } response := dto.PaginatedResponse{ Data: companies, Pagination: dto.Pagination{ Page: page, Limit: limit, Total: total, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *AdminHandlers) UpdateCompanyStatus(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req UpdateCompanyStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid Request", http.StatusBadRequest) return } company, err := h.adminService.UpdateCompanyStatus(r.Context(), id, req.Active, req.Verified) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Notify Company Owner if req.Verified != nil { statusText := "rejeitada" if *req.Verified { statusText = "aprovada" } // Find owner of the company to notify owner, err := h.adminService.GetCompanyOwner(r.Context(), id) if err == nil && owner != nil { title := "Status da Empresa Atualizado" msg := "Sua empresa '" + company.Name + "' foi " + statusText + " pela moderação." link := "/dashboard/profile" // This call now automatically publishes to LavinMQ for async Push/Email _ = h.notificationService.CreateNotification(r.Context(), owner.ID, "system", title, msg, &link) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(company) } func (h *AdminHandlers) ListJobs(w http.ResponseWriter, r *http.Request) { ctx := r.Context() page, _ := strconv.Atoi(r.URL.Query().Get("page")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) status := r.URL.Query().Get("status") // Extract role and companyID for scoping roles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles)) isSuperadmin := false for _, role := range roles { if role == "SUPERADMIN" || role == "superadmin" { isSuperadmin = true break } } filter := dto.JobFilterQuery{ PaginationQuery: dto.PaginationQuery{ Page: page, Limit: limit, }, } if status != "" { filter.Status = &status } // If Admin (not Superadmin), scope to their company if !isSuperadmin { companyID, _ := ctx.Value(middleware.ContextTenantID).(string) if companyID != "" { filter.CompanyID = &companyID } } jobs, total, err := h.jobService.GetJobs(filter) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } response := dto.PaginatedResponse{ Data: jobs, Pagination: dto.Pagination{ Page: page, Limit: limit, Total: total, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *AdminHandlers) UpdateJobStatus(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req UpdateJobStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid Request", http.StatusBadRequest) return } if req.Status == "" { http.Error(w, "Status is required", http.StatusBadRequest) return } status := req.Status job, err := h.jobService.UpdateJob(id, dto.UpdateJobRequest{Status: &status}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(job) } func (h *AdminHandlers) DuplicateJob(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") job, err := h.adminService.DuplicateJob(r.Context(), id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(job) } func (h *AdminHandlers) ListTags(w http.ResponseWriter, r *http.Request) { category := r.URL.Query().Get("category") if category == "" { category = "" } var categoryFilter *string if category != "" { categoryFilter = &category } tags, err := h.adminService.ListTags(r.Context(), categoryFilter) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tags) } func (h *AdminHandlers) CreateTag(w http.ResponseWriter, r *http.Request) { var req CreateTagRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid Request", http.StatusBadRequest) return } if req.Name == "" || req.Category == "" { http.Error(w, "Name and category are required", http.StatusBadRequest) return } tag, err := h.adminService.CreateTag(r.Context(), req.Name, req.Category) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(tag) } func (h *AdminHandlers) UpdateTag(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid tag ID", http.StatusBadRequest) return } var req UpdateTagRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid Request", http.StatusBadRequest) return } tag, err := h.adminService.UpdateTag(r.Context(), id, req.Name, req.Active) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tag) } func (h *AdminHandlers) ListCandidates(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Extract role for scoping roles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles)) isSuperadmin := false for _, role := range roles { if role == "SUPERADMIN" || role == "superadmin" { isSuperadmin = true break } } var companyID *string if !isSuperadmin { if cid, ok := ctx.Value(middleware.ContextTenantID).(string); ok && cid != "" { companyID = &cid } } // Parse pagination params page := 1 perPage := 10 if p := r.URL.Query().Get("page"); p != "" { if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { page = parsed } } if pp := r.URL.Query().Get("perPage"); pp != "" { if parsed, err := strconv.Atoi(pp); err == nil && parsed > 0 && parsed <= 100 { perPage = parsed } } candidates, stats, pagination, err := h.adminService.ListCandidates(ctx, companyID, page, perPage) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } response := dto.CandidateListResponse{ Stats: stats, Candidates: candidates, Pagination: pagination, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // UpdateCompany updates a company's information // @Summary Update Company // @Description Updates company information by ID (admin only) // @Tags Companies // @Accept json // @Produce json // @Param id path string true "Company ID" // @Param body body dto.UpdateCompanyRequest true "Company update data" // @Success 200 {object} object // @Failure 400 {string} string "Invalid Request" // @Failure 401 {string} string "Unauthorized" // @Failure 500 {string} string "Internal Server Error" // @Security BearerAuth // @Router /api/v1/companies/{id} [patch] func (h *AdminHandlers) UpdateCompany(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var req dto.UpdateCompanyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid Request", http.StatusBadRequest) return } company, err := h.adminService.UpdateCompany(r.Context(), id, req) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(company) } // DeleteCompany deletes a company // @Summary Delete Company // @Description Deletes a company by ID (admin only) // @Tags Companies // @Param id path string true "Company ID" // @Success 204 "No Content" // @Failure 401 {string} string "Unauthorized" // @Failure 500 {string} string "Internal Server Error" // @Security BearerAuth // @Router /api/v1/companies/{id} [delete] func (h *AdminHandlers) DeleteCompany(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := h.adminService.DeleteCompany(r.Context(), id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (h *AdminHandlers) PurgeCache(w http.ResponseWriter, r *http.Request) { if err := h.cloudflareService.PurgeCache(r.Context()); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"message": "Cache purge requested"}) } // ============================================================================ // Email Templates CRUD Handlers // ============================================================================ func (h *AdminHandlers) ListEmailTemplates(w http.ResponseWriter, r *http.Request) { templates, err := h.adminService.ListEmailTemplates(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(templates) } func (h *AdminHandlers) GetEmailTemplate(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") tmpl, err := h.adminService.GetEmailTemplate(r.Context(), slug) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if tmpl == nil { http.Error(w, "Template not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tmpl) } func (h *AdminHandlers) CreateEmailTemplate(w http.ResponseWriter, r *http.Request) { var req dto.CreateEmailTemplateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if req.Slug == "" || req.Subject == "" { http.Error(w, "Slug and subject are required", http.StatusBadRequest) return } tmpl, err := h.adminService.CreateEmailTemplate(r.Context(), req) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(tmpl) } func (h *AdminHandlers) UpdateEmailTemplate(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") var req dto.UpdateEmailTemplateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } tmpl, err := h.adminService.UpdateEmailTemplate(r.Context(), slug, req) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tmpl) } func (h *AdminHandlers) DeleteEmailTemplate(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if err := h.adminService.DeleteEmailTemplate(r.Context(), slug); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // ============================================================================ // Email Settings Handlers // ============================================================================ func (h *AdminHandlers) GetEmailSettings(w http.ResponseWriter, r *http.Request) { settings, err := h.adminService.GetEmailSettings(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if settings == nil { // Return empty object with defaults settings = &dto.EmailSettingsDTO{ Provider: "smtp", SMTPSecure: true, SenderName: "GoHorse Jobs", SenderEmail: "no-reply@gohorsejobs.com", } } // Mask password in response if settings.SMTPPass != nil { masked := "********" settings.SMTPPass = &masked } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(settings) } func (h *AdminHandlers) UpdateEmailSettings(w http.ResponseWriter, r *http.Request) { var req dto.UpdateEmailSettingsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } settings, err := h.adminService.UpdateEmailSettings(r.Context(), req) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Mask password in response if settings.SMTPPass != nil { masked := "********" settings.SMTPPass = &masked } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(settings) }