feat(api): REST на chi + huma с auth-ручками и OpenAPI 3.1

- переход на chi + huma (решение D-006): huma отдаёт типизированные ручки и
  генерирует документ, auth-ручки живут на chi (cookie и заголовки напрямую)
- единый формат ошибок: код (auth.invalid_credentials, auth.2fa_required,
  perm.denied и т.д.) + человекочитаемое сообщение (AGENT.md 8.5)
- cookie сессии __Host-session: HttpOnly, SameSite=Lax, Secure при TLS;
  альтернатива — Bearer-токен для desktop/CLI (AGENT.md 8.1)
- ручки: register, login, logout, logout-all, sessions, step-up, 2fa/setup,
  2fa/enable, users/@me; IP и User-Agent прокидываются из запроса в контекст
- /api/v1/openapi.json: объединённый документ (схемы huma + контракт auth)
- тесты: регистрация через API с cookie, ошибки входа, обязательная сессия,
  валидация, наличие всех путей в OpenAPI
This commit is contained in:
2026-09-19 21:36:00 +03:00
parent 16218ee045
commit f61751ed26
12 changed files with 984 additions and 148 deletions
+31
View File
@@ -1,8 +1,10 @@
package httpx
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
)
func newRequestID() string {
@@ -12,3 +14,32 @@ func newRequestID() string {
}
return hex.EncodeToString(buf[:])
}
type requestInfoKey struct{}
// RequestInfo — данные исходного запроса, нужные сервисам (IP, User-Agent).
type RequestInfo struct {
IP string
UserAgent string
}
// WithRequestInfo кладёт данные запроса в контекст (используется HTTP-слоем).
func WithRequestInfo(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, requestInfoKey{}, RequestInfo{
IP: ClientIP(r, nil),
UserAgent: r.Header.Get("User-Agent"),
})
}
func requestInfo(ctx context.Context) RequestInfo {
if info, ok := ctx.Value(requestInfoKey{}).(RequestInfo); ok {
return info
}
return RequestInfo{}
}
// ClientIPFromContext возвращает IP клиента для контекста huma.
func ClientIPFromContext(ctx context.Context) string { return requestInfo(ctx).IP }
// UserAgentFromContext возвращает User-Agent для контекста huma.
func UserAgentFromContext(ctx context.Context) string { return requestInfo(ctx).UserAgent }
+8
View File
@@ -44,6 +44,14 @@ func (r *statusRecorder) Flush() {
}
}
// RequestInfoMiddleware кладёт IP и User-Agent запроса в контекст: huma-хендлеры
// не получают *http.Request, а сервисам эти данные нужны (аудит, безопасность).
func RequestInfoMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(WithRequestInfo(r.Context(), r)))
})
}
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-Id")