360 lines
9.0 KiB
Go
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)
|
|
}
|