Files
pipu-music/services/api/internal/platform/httpx/problem.go
T
mrleo1nidandClaude Opus 5.5 ed4a492dba
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m15s
ci / api / api (push) Successful in 1m31s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 39s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 26s
feat(api): sign-in, refresh rotation and bootstrap admin (M1)
- users, refresh_tokens and audit_log schema (migration 00002)
- argon2id passwords with rehash on login, password policy, lockout
  with exponential back-off and a per-IP rate limit in Redis
- Ed25519 JWT access tokens with kid-based key rotation; rotating
  refresh tokens with reuse detection (cookie for web, body for Android)
- bootstrap admin on first start; must_change_credentials and
  must_change_password gate every other endpoint
- /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password,
  /me/bootstrap-credentials, /me/sessions; server-info reports
  bootstrap_required
- role policy from docs/auth-and-roles.md with table tests
- problem+json errors with field errors; ru/en translations of the codes
- install.sh and update.sh generate JWT_PRIVATE_KEY

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:12:25 +03:00

70 lines
2.8 KiB
Go

// Package httpx — общие HTTP-утилиты: ответы об ошибках (RFC 9457), логирование, middleware.
package httpx
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"strconv"
"pipu/api/internal/platform/apperr"
)
// Problem — тело ответа application/problem+json. Поле Code переводится клиентом через i18n.
type Problem struct {
Type string `json:"type"`
Status int `json:"status"`
Code string `json:"code"`
Detail string `json:"detail,omitempty"`
Params map[string]any `json:"params,omitempty"`
Errors []apperr.FieldError `json:"errors,omitempty"`
}
// WriteProblem пишет ответ об ошибке.
func WriteProblem(w http.ResponseWriter, status int, code, detail string) {
writeProblem(w, Problem{Type: "about:blank", Status: status, Code: code, Detail: detail})
}
func writeProblem(w http.ResponseWriter, p Problem) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(p.Status)
_ = json.NewEncoder(w).Encode(p)
}
// WriteError пишет доменную ошибку *apperr.Error; любую другую — как 500 (подробности в лог).
func WriteError(log *slog.Logger, w http.ResponseWriter, r *http.Request, err error) {
var ae *apperr.Error
if !errors.As(err, &ae) {
log.ErrorContext(r.Context(), "handler failed", "path", r.URL.Path, "err", err)
WriteProblem(w, http.StatusInternalServerError, "common.internal", "internal error")
return
}
if ae.RetryAfter > 0 {
w.Header().Set("Retry-After", strconv.Itoa(ae.RetryAfter))
}
writeProblem(w, Problem{
Type: "about:blank", Status: ae.Status, Code: ae.Code, Detail: ae.Detail, Params: ae.Params, Errors: ae.Fields,
})
}
// NotFound — обработчик неизвестных путей.
func NotFound(w http.ResponseWriter, _ *http.Request) {
WriteProblem(w, http.StatusNotFound, "common.not_found", "route not found")
}
// MethodNotAllowed — обработчик неподдерживаемых методов.
func MethodNotAllowed(w http.ResponseWriter, _ *http.Request) {
WriteProblem(w, http.StatusMethodNotAllowed, "common.method_not_allowed", "method not allowed")
}
// RequestError — ошибка разбора запроса (strict-server).
func RequestError(w http.ResponseWriter, _ *http.Request, err error) {
WriteProblem(w, http.StatusBadRequest, "common.bad_request", err.Error())
}
// ResponseError — ошибка обработчика: доменные — как есть, прочие — 500 с деталями только в логе.
func ResponseError(log *slog.Logger) func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, r *http.Request, err error) { WriteError(log, w, r, err) }
}