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) deleteAccount(w http.ResponseWriter, r *http.Request) { id, ok := pathID(w, r) if !ok { return } if id == sessionFrom(r).AccountID { a.writeError(w, domain.InvalidState("você não pode excluir a própria conta")) return } if err := a.svc.DeleteAccount(r.Context(), actorOf(r), id); err != nil { a.writeError(w, err) return } w.WriteHeader(http.StatusNoContent) } 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) }