Projeto bot pontos
This commit is contained in:
152
backend/internal/httpapi/request.go
Normal file
152
backend/internal/httpapi/request.go
Normal file
@@ -0,0 +1,152 @@
|
||||
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}}
|
||||
}
|
||||
Reference in New Issue
Block a user