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

360 lines
9.0 KiB
Go

package httpapi
import (
"encoding/json"
"net/http"
"strconv"
"botpontos/internal/adapters/postgres"
"botpontos/internal/app"
"botpontos/internal/domain"
"botpontos/internal/httpx"
)
// --- notificações ---
func (a *API) listNotifications(w http.ResponseWriter, r *http.Request) {
userID, err := queryInt64(r, "user_id")
if err != nil {
a.writeError(w, err)
return
}
if account := sessionFrom(r).Account; isRestricted(account) {
userID = account.UserID // ignora qualquer filtro enviado: só os próprios lembretes
}
status, typ := queryString(r, "status"), queryString(r, "type")
if status != nil && !domain.ValidNotificationStatus(*status) {
a.writeError(w, badRequest("status", "status inválido"))
return
}
if typ != nil && !domain.ValidNotificationType(*typ) {
a.writeError(w, badRequest("type", "use entrada2 ou saida2"))
return
}
day := queryString(r, "day")
if day != nil {
if _, err := queryDate(r, "day", a.svc.Location(), false); err != nil {
a.writeError(w, err)
return
}
}
items, err := a.svc.Store().ListNotifications(r.Context(), postgres.NotificationFilter{
Day: day, Status: status, Type: typ, UserID: userID, Limit: 200,
})
if err != nil {
a.writeError(w, err)
return
}
out := make([]notificationJSON, 0, len(items))
for _, v := range items {
out = append(out, toNotification(v.Notification, v.UserName))
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"notifications": out})
}
func (a *API) cancelNotification(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
n, err := a.svc.CancelNotification(r.Context(), actorOf(r), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toNotification(n, ""))
}
func (a *API) resendNotification(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
n, err := a.svc.ResendNotification(r.Context(), actorOf(r), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toNotification(n, ""))
}
// --- auditoria ---
func (a *API) listAudit(w http.ResponseWriter, r *http.Request) {
loc := a.svc.Location()
from, err := queryDate(r, "from", loc, false)
if err != nil {
a.writeError(w, err)
return
}
to, err := queryDate(r, "to", loc, true)
if err != nil {
a.writeError(w, err)
return
}
success, err := queryBool(r, "success")
if err != nil {
a.writeError(w, err)
return
}
target, err := queryInt64(r, "target_user_id")
if err != nil {
a.writeError(w, err)
return
}
account, err := queryInt64(r, "actor_account")
if err != nil {
a.writeError(w, err)
return
}
before, err := queryInt64(r, "before")
if err != nil {
a.writeError(w, err)
return
}
const limit = 50
logs, err := a.svc.Store().ListAudit(r.Context(), postgres.AuditFilter{
From: from, To: to, Source: queryString(r, "source"), EventType: queryString(r, "event_type"),
Success: success, TargetUserID: target, ActorAccount: account, Before: before, Limit: limit,
})
if err != nil {
a.writeError(w, err)
return
}
out := make([]auditJSON, 0, len(logs))
for _, l := range logs {
out = append(out, toAudit(l))
}
var next *int64
if len(logs) == limit {
next = &logs[len(logs)-1].ID
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"logs": out, "next_before": next})
}
// --- feriados ---
func (a *API) yearParam(r *http.Request) (int, error) {
v := queryString(r, "year")
if v == nil {
return a.svc.Now().In(a.svc.Location()).Year(), nil
}
year, err := strconv.Atoi(*v)
if err != nil || year < 2000 || year > 2100 {
return 0, badRequest("year", "ano inválido")
}
return year, nil
}
func (a *API) listHolidays(w http.ResponseWriter, r *http.Request) {
year, err := a.yearParam(r)
if err != nil {
a.writeError(w, err)
return
}
holidays, err := a.svc.ListHolidays(r.Context(), year)
if err != nil {
a.writeError(w, err)
return
}
out := make([]holidayJSON, 0, len(holidays))
for _, h := range holidays {
out = append(out, holidayJSON{Day: h.Day, Name: h.Name, Source: h.Source})
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"year": year, "holidays": out})
}
func (a *API) addHoliday(w http.ResponseWriter, r *http.Request) {
var body holidayJSON
if !decode(w, r, &body) {
return
}
if err := a.svc.AddHoliday(r.Context(), actorOf(r), body.Day, body.Name); err != nil {
a.writeError(w, err)
return
}
w.WriteHeader(http.StatusCreated)
}
func (a *API) deleteHoliday(w http.ResponseWriter, r *http.Request) {
if err := a.svc.DeleteHoliday(r.Context(), actorOf(r), r.PathValue("day")); err != nil {
a.writeError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (a *API) syncHolidays(w http.ResponseWriter, r *http.Request) {
var body struct {
Year int `json:"year"`
}
if !decode(w, r, &body) {
return
}
if body.Year < 2000 || body.Year > 2100 {
a.writeError(w, badRequest("year", "ano inválido"))
return
}
inserted, err := a.svc.SyncHolidays(r.Context(), actorOf(r), body.Year)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"inserted": inserted})
}
// --- configurações globais ---
func (a *API) getSettings(w http.ResponseWriter, r *http.Request) {
window, err := a.svc.GlobalWindow(r.Context())
if err != nil {
a.writeError(w, err)
return
}
jobs := a.svc.Config().Jobs
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"window": toWindow(window),
"reconciliation_minutes": int(jobs.ReconciliationInterval.Minutes()),
"dispatch_minutes": int(jobs.DispatchInterval.Minutes()),
"late_tolerance_minutes": int(jobs.LateTolerance.Minutes()),
"api_failure_threshold": jobs.APIFailureThreshold,
"slow_check_minutes": int(jobs.SlowCheckInterval.Minutes()),
})
}
func (a *API) updateSettings(w http.ResponseWriter, r *http.Request) {
var body windowJSON
if !decode(w, r, &body) {
return
}
window, err := body.parse("")
if err == nil {
err = a.svc.UpdateGlobalWindow(r.Context(), actorOf(r), window)
}
if err != nil {
a.writeError(w, err)
return
}
a.getSettings(w, r)
}
func (a *API) previewSettings(w http.ResponseWriter, r *http.Request) {
var body windowJSON
if !decode(w, r, &body) {
return
}
window, err := body.parse("")
if err != nil {
a.writeError(w, err)
return
}
preview, err := a.svc.PreviewWindow(r.Context(), window)
if err != nil {
a.writeError(w, err)
return
}
if preview.Errors == nil {
preview.Errors = domain.ValidationErrors{}
}
httpx.WriteJSON(w, http.StatusOK, preview)
}
// --- contas do painel (owner) ---
func (a *API) listAccounts(w http.ResponseWriter, r *http.Request) {
accounts, err := a.svc.Store().ListWebAccounts(r.Context())
if err != nil {
a.writeError(w, err)
return
}
out := make([]accountJSON, 0, len(accounts))
for _, acc := range accounts {
out = append(out, toAccount(acc))
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"accounts": out})
}
func (a *API) createAccount(w http.ResponseWriter, r *http.Request) {
var body struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
UserID *int64 `json:"user_id"`
}
if !decode(w, r, &body) {
return
}
account, err := a.svc.CreateAccount(r.Context(), actorOf(r), app.AccountInput{
Username: body.Username, Password: body.Password, Role: domain.Role(body.Role), UserID: body.UserID,
})
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusCreated, toAccount(account))
}
func (a *API) updateAccount(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var body struct {
Role *string `json:"role"`
Disabled *bool `json:"disabled"`
UserID json.RawMessage `json:"user_id"` // ausente: mantém; null: remove o vínculo
}
if !decode(w, r, &body) {
return
}
var link *app.AccountUserLink
if body.UserID != nil {
link = &app.AccountUserLink{}
if string(body.UserID) != "null" {
var uid int64
if err := json.Unmarshal(body.UserID, &uid); err != nil || uid <= 0 {
a.writeError(w, badRequest("user_id", "informe o id do usuário ou null"))
return
}
link.UserID = &uid
}
}
var role *domain.Role
if body.Role != nil {
v := domain.Role(*body.Role)
role = &v
}
if id == sessionFrom(r).AccountID && body.Disabled != nil && *body.Disabled {
a.writeError(w, domain.InvalidState("você não pode desativar a própria conta"))
return
}
if err := a.svc.UpdateAccount(r.Context(), actorOf(r), id, role, body.Disabled, link); err != nil {
a.writeError(w, err)
return
}
account, err := a.svc.Store().GetWebAccount(r.Context(), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toAccount(account))
}
func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var body struct {
Password string `json:"password"`
}
if !decode(w, r, &body) {
return
}
if err := a.svc.ResetPassword(r.Context(), actorOf(r), id, body.Password); err != nil {
a.writeError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}