// 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("DELETE /api/v1/accounts/{id}", a.auth(domain.RoleOwner, a.deleteAccount)) 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) }) }