90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"browser.local/platform/dto"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/service"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
const (
|
|
errorCodeForbidden = "forbidden"
|
|
errorCodeBadRequest = "bad_request"
|
|
errorCodeDuplicate = "duplicate_resource"
|
|
errorCodeInternal = "internal_error"
|
|
errorCodeMethodNotAllowed = "method_not_allowed"
|
|
errorCodeNotFound = "not_found"
|
|
errorCodeUnauthorized = "unauthorized"
|
|
errorCodeValidation = "validation_failed"
|
|
)
|
|
|
|
func decodeJSON[T any](r *http.Request) (T, error) {
|
|
var value T
|
|
decoder := json.NewDecoder(r.Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&value); err != nil {
|
|
return value, fmt.Errorf("decode json: %w", err)
|
|
}
|
|
|
|
var extra struct{}
|
|
if err := decoder.Decode(&extra); err != io.EOF {
|
|
return value, errors.New("decode json: multiple JSON values are not allowed")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, value any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|
|
|
|
func writeAPIError(w http.ResponseWriter, status int, code string, message string, details []string) {
|
|
writeJSON(w, status, dto.ErrorResponse{
|
|
Code: code,
|
|
Message: message,
|
|
Details: details,
|
|
})
|
|
}
|
|
|
|
func writeMethodNotAllowed(w http.ResponseWriter, allow string) {
|
|
w.Header().Set("Allow", allow)
|
|
writeAPIError(w, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed, "method not allowed", nil)
|
|
}
|
|
|
|
func writeServiceError(w http.ResponseWriter, err error) {
|
|
var validationErr validator.ValidationError
|
|
var forbiddenErr service.ForbiddenError
|
|
switch {
|
|
case errors.As(err, &validationErr):
|
|
writeAPIError(w, http.StatusBadRequest, errorCodeValidation, "validation failed", validationErr.Violations)
|
|
case errors.Is(err, service.ErrUnauthorized):
|
|
writeAPIError(w, http.StatusUnauthorized, errorCodeUnauthorized, "authentication required", nil)
|
|
case errors.As(err, &forbiddenErr):
|
|
message := strings.TrimSpace(forbiddenErr.Reason)
|
|
if message == "" {
|
|
message = "account is not allowed to access this resource"
|
|
}
|
|
writeAPIError(w, http.StatusForbidden, errorCodeForbidden, message, nil)
|
|
case errors.Is(err, service.ErrForbidden):
|
|
writeAPIError(w, http.StatusForbidden, errorCodeForbidden, "account is not allowed to access this resource", nil)
|
|
case errors.Is(err, repo.ErrDuplicate):
|
|
writeAPIError(w, http.StatusConflict, errorCodeDuplicate, "resource already exists", nil)
|
|
case errors.Is(err, repo.ErrNotFound):
|
|
writeAPIError(w, http.StatusNotFound, errorCodeNotFound, "resource not found", nil)
|
|
default:
|
|
writeAPIError(w, http.StatusInternalServerError, errorCodeInternal, "internal server error", nil)
|
|
}
|
|
}
|
|
|
|
func writeDecodeError(w http.ResponseWriter, err error) {
|
|
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid JSON request body", []string{err.Error()})
|
|
}
|