Files
bot-pontos/backend/internal/httpapi/handler.go
2026-10-23 22:31:02 -03:00

112 lines
4.8 KiB
Go

// Package httpapi é o adapter de entrada HTTP: a API REST consumida pelo Next.js
// (ARQUITETURA.md, seções 5.4 e 7).
package httpapi
import (
"context"
"log/slog"
"net/http"
"time"
"botpontos/internal/app"
"botpontos/internal/config"
"botpontos/internal/domain"
"botpontos/internal/httpx"
)
// Pinger é satisfeito por *pgxpool.Pool; a interface permite testar sem banco.
type Pinger interface {
Ping(context.Context) error
}
type API struct {
db Pinger
svc *app.Service
cfg config.Config
log *slog.Logger
}
// NewHandler monta as rotas. svc pode ser nil nos testes das rotas de saúde.
func NewHandler(db Pinger, svc *app.Service, cfg config.Config, logger *slog.Logger) http.Handler {
a := &API{db: db, svc: svc, cfg: cfg, log: logger}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/health/live", a.live)
mux.HandleFunc("GET /api/v1/health/ready", a.ready)
mux.HandleFunc("POST /api/v1/auth/login", a.login)
mux.Handle("POST /api/v1/auth/logout", a.auth(domain.RoleViewer, a.logout))
mux.Handle("GET /api/v1/auth/me", a.auth(domain.RoleViewer, a.me))
mux.Handle("GET /api/v1/dashboard", a.auth(domain.RoleViewer, a.dashboard))
mux.Handle("GET /api/v1/users", a.auth(domain.RoleViewer, a.listUsers))
mux.Handle("GET /api/v1/users/{id}", a.authUser(domain.RoleViewer, false, a.getUser))
mux.Handle("POST /api/v1/users/{id}/activate", a.auth(domain.RoleAdmin, a.activateUser))
mux.Handle("POST /api/v1/users/{id}/disable", a.auth(domain.RoleAdmin, a.disableUser))
mux.Handle("PUT /api/v1/users/{id}/schedule", a.authUser(domain.RoleAdmin, true, a.updateSchedule))
mux.Handle("PUT /api/v1/users/{id}/credentials", a.authUser(domain.RoleAdmin, true, a.updateCredentials))
mux.Handle("POST /api/v1/users/{id}/credentials/test", a.authUser(domain.RoleAdmin, true, a.testCredentials))
mux.Handle("POST /api/v1/users/{id}/notifications/enable", a.authUser(domain.RoleAdmin, true, a.enableNotifications))
mux.Handle("POST /api/v1/users/{id}/notifications/disable", a.authUser(domain.RoleAdmin, true, a.disableNotifications))
mux.Handle("PUT /api/v1/users/{id}/bot-admin", a.auth(domain.RoleAdmin, a.setBotAdmin))
mux.Handle("GET /api/v1/users/{id}/today", a.authUser(domain.RoleViewer, false, a.today))
mux.Handle("POST /api/v1/users/{id}/today/resume", a.auth(domain.RoleAdmin, a.resumeDay))
mux.Handle("GET /api/v1/notifications", a.auth(domain.RoleViewer, a.listNotifications))
mux.Handle("POST /api/v1/notifications/{id}/cancel", a.auth(domain.RoleAdmin, a.cancelNotification))
mux.Handle("POST /api/v1/notifications/{id}/resend", a.auth(domain.RoleAdmin, a.resendNotification))
mux.Handle("GET /api/v1/audit", a.authUnrestricted(domain.RoleViewer, a.listAudit))
mux.Handle("GET /api/v1/holidays", a.auth(domain.RoleViewer, a.listHolidays))
mux.Handle("POST /api/v1/holidays", a.auth(domain.RoleAdmin, a.addHoliday))
mux.Handle("POST /api/v1/holidays/sync", a.auth(domain.RoleAdmin, a.syncHolidays))
mux.Handle("DELETE /api/v1/holidays/{day}", a.auth(domain.RoleAdmin, a.deleteHoliday))
mux.Handle("GET /api/v1/settings", a.authUnrestricted(domain.RoleViewer, a.getSettings))
mux.Handle("PUT /api/v1/settings", a.auth(domain.RoleAdmin, a.updateSettings))
mux.Handle("POST /api/v1/settings/preview", a.auth(domain.RoleAdmin, a.previewSettings))
mux.Handle("GET /api/v1/accounts", a.auth(domain.RoleOwner, a.listAccounts))
mux.Handle("POST /api/v1/accounts", a.auth(domain.RoleOwner, a.createAccount))
mux.Handle("PATCH /api/v1/accounts/{id}", a.auth(domain.RoleOwner, a.updateAccount))
mux.Handle("POST /api/v1/accounts/{id}/reset-password", a.auth(domain.RoleOwner, a.resetPassword))
mux.HandleFunc("/", notFound)
return httpx.Recover(logger, httpx.RequestLog(logger, securityHeaders(mux)))
}
// live indica só que o processo está de pé (usado pelo healthcheck do Docker).
func (a *API) live(w http.ResponseWriter, _ *http.Request) {
httpx.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// ready indica que a API consegue atender, ou seja, que o Postgres responde.
func (a *API) ready(w http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 2*time.Second)
defer cancel()
if err := a.db.Ping(ctx); err != nil {
a.log.Warn("banco de dados indisponível", "error", err)
httpx.WriteProblem(w, http.StatusServiceUnavailable, "Banco de dados indisponível", "DB_UNAVAILABLE")
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "database": "ok"})
}
func notFound(w http.ResponseWriter, _ *http.Request) {
httpx.WriteProblem(w, http.StatusNotFound, "Recurso não encontrado", "NOT_FOUND")
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Cache-Control", "no-store")
h.Set("Referrer-Policy", "same-origin")
next.ServeHTTP(w, r)
})
}