153 lines
5.0 KiB
Go
153 lines
5.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"botpontos/internal/adapters/pontomais"
|
|
"botpontos/internal/app"
|
|
"botpontos/internal/domain"
|
|
"botpontos/internal/httpx"
|
|
)
|
|
|
|
const maxBodyBytes = 64 << 10
|
|
|
|
// decode lê o corpo JSON recusando campos desconhecidos.
|
|
func decode(w http.ResponseWriter, r *http.Request, out any) bool {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(out); err != nil {
|
|
title := "Corpo da requisição inválido"
|
|
if errors.Is(err, io.EOF) {
|
|
title = "Corpo da requisição vazio"
|
|
}
|
|
httpx.WriteProblem(w, http.StatusBadRequest, title, "BAD_REQUEST")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func pathID(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
httpx.WriteProblem(w, http.StatusBadRequest, "Identificador inválido", "BAD_REQUEST")
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
func queryString(r *http.Request, key string) *string {
|
|
v := strings.TrimSpace(r.URL.Query().Get(key))
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
return &v
|
|
}
|
|
|
|
func queryInt64(r *http.Request, key string) (*int64, error) {
|
|
v := queryString(r, key)
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
n, err := strconv.ParseInt(*v, 10, 64)
|
|
if err != nil {
|
|
return nil, domain.ValidationErrors{{Field: key, Message: "número inválido"}}
|
|
}
|
|
return &n, nil
|
|
}
|
|
|
|
func queryBool(r *http.Request, key string) (*bool, error) {
|
|
v := queryString(r, key)
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
b, err := strconv.ParseBool(*v)
|
|
if err != nil {
|
|
return nil, domain.ValidationErrors{{Field: key, Message: "use true ou false"}}
|
|
}
|
|
return &b, nil
|
|
}
|
|
|
|
// queryDate lê AAAA-MM-DD; end=true devolve o início do dia seguinte (limite exclusivo).
|
|
func queryDate(r *http.Request, key string, loc *time.Location, end bool) (*time.Time, error) {
|
|
v := queryString(r, key)
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
t, err := time.ParseInLocation(domain.DateLayout, *v, loc)
|
|
if err != nil {
|
|
return nil, domain.ValidationErrors{{Field: key, Message: "data inválida (use AAAA-MM-DD)"}}
|
|
}
|
|
if end {
|
|
t = t.AddDate(0, 0, 1)
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// writeError traduz os erros do domínio em respostas problem+json.
|
|
func (a *API) writeError(w http.ResponseWriter, err error) {
|
|
var validation domain.ValidationErrors
|
|
var credErr *app.CredentialError
|
|
switch {
|
|
case errors.As(err, &validation):
|
|
httpx.WriteProblemDetails(w, httpx.Problem{
|
|
Title: "Dados inválidos", Status: http.StatusUnprocessableEntity, Code: "VALIDATION", Errors: validation,
|
|
})
|
|
case errors.As(err, &credErr):
|
|
if errors.Is(err, pontomais.ErrAuthRejected) {
|
|
httpx.WriteProblem(w, http.StatusUnprocessableEntity,
|
|
"A PontoMais recusou o e-mail ou a senha; a credencial anterior foi mantida", "CREDENTIALS_REJECTED")
|
|
return
|
|
}
|
|
httpx.WriteProblem(w, http.StatusBadGateway,
|
|
"Não foi possível validar na PontoMais agora; a credencial anterior foi mantida", "POINTS_UNAVAILABLE")
|
|
case errors.Is(err, domain.ErrNotFound):
|
|
httpx.WriteProblem(w, http.StatusNotFound, "Registro não encontrado", "NOT_FOUND")
|
|
case errors.Is(err, domain.ErrCredentialsRequired):
|
|
httpx.WriteProblem(w, http.StatusConflict, "Configure e valide a credencial da PontoMais antes", "CREDENTIALS_REQUIRED")
|
|
case errors.Is(err, domain.ErrUserNotActive):
|
|
httpx.WriteProblem(w, http.StatusConflict, "O usuário precisa estar ativo", "USER_NOT_ACTIVE")
|
|
case errors.Is(err, domain.ErrLastOwner):
|
|
httpx.WriteProblem(w, http.StatusConflict, "É preciso manter pelo menos um owner ativo", "LAST_OWNER")
|
|
case errors.Is(err, domain.ErrInvalidState):
|
|
httpx.WriteProblem(w, http.StatusConflict, capitalize(err.Error()), "INVALID_STATE")
|
|
case errors.Is(err, domain.ErrInvalidLogin):
|
|
httpx.WriteProblem(w, http.StatusUnauthorized, "Usuário ou senha inválidos", "INVALID_LOGIN")
|
|
case errors.Is(err, domain.ErrAccountLocked):
|
|
httpx.WriteProblem(w, http.StatusLocked, "Conta bloqueada por tentativas de login; tente mais tarde", "ACCOUNT_LOCKED")
|
|
case errors.Is(err, domain.ErrNotConfigured):
|
|
httpx.WriteProblem(w, http.StatusServiceUnavailable, "Integração não configurada no servidor", "NOT_CONFIGURED")
|
|
case errors.Is(err, pontomais.ErrAuthRejected):
|
|
httpx.WriteProblem(w, http.StatusBadGateway, "A PontoMais recusou a credencial gravada", "CREDENTIALS_REJECTED")
|
|
case isPointsError(err):
|
|
httpx.WriteProblem(w, http.StatusBadGateway, "A PontoMais não respondeu; tente novamente", "POINTS_UNAVAILABLE")
|
|
default:
|
|
a.log.Error("erro interno na API", "error", err)
|
|
httpx.WriteProblem(w, http.StatusInternalServerError, "Erro interno", "INTERNAL")
|
|
}
|
|
}
|
|
|
|
func isPointsError(err error) bool {
|
|
return strings.Contains(err.Error(), "PontoMais")
|
|
}
|
|
|
|
func capitalize(s string) string {
|
|
first, size := utf8.DecodeRuneInString(s)
|
|
if first == utf8.RuneError {
|
|
return s
|
|
}
|
|
return string(unicode.ToUpper(first)) + s[size:]
|
|
}
|
|
|
|
func badRequest(field, message string) error {
|
|
return domain.ValidationErrors{{Field: field, Message: message}}
|
|
}
|