Files
pipu-music/services/api/internal/platform/apperr/apperr.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

72 lines
2.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package apperr — доменные ошибки с машинным кодом для i18n. В problem+json их переводит
// единственное место — httpx.ResponseError; модули только возвращают *Error.
package apperr
import (
"fmt"
"net/http"
)
// FieldError — ошибка конкретного поля запроса.
type FieldError struct {
Field string `json:"field"`
Code string `json:"code"`
}
// Error — ошибка, которую можно показать клиенту: HTTP-статус, код и параметры для перевода.
type Error struct {
Status int
Code string
Detail string
Params map[string]any
Fields []FieldError
// RetryAfter — секунды для заголовка Retry-After (429).
RetryAfter int
}
func (e *Error) Error() string {
if e.Detail != "" {
return fmt.Sprintf("%s: %s", e.Code, e.Detail)
}
return e.Code
}
// Is сравнивает ошибки по коду: errors.Is(err, apperr.Unauthorized("auth.invalid_credentials", "")).
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
return ok && t.Code == e.Code
}
// New создаёт ошибку с произвольным статусом.
func New(status int, code, detail string) *Error {
return &Error{Status: status, Code: code, Detail: detail}
}
// Unauthorized — 401.
func Unauthorized(code, detail string) *Error { return New(http.StatusUnauthorized, code, detail) }
// Forbidden — 403.
func Forbidden(code, detail string) *Error { return New(http.StatusForbidden, code, detail) }
// NotFound — 404.
func NotFound(code, detail string) *Error { return New(http.StatusNotFound, code, detail) }
// Conflict — 409.
func Conflict(code, detail string) *Error { return New(http.StatusConflict, code, detail) }
// Validation — 422 с ошибкой одного поля.
func Validation(field, code, detail string, params map[string]any) *Error {
return &Error{
Status: http.StatusUnprocessableEntity,
Code: code,
Detail: detail,
Params: params,
Fields: []FieldError{{Field: field, Code: code}},
}
}
// TooManyRequests — 429 с Retry-After.
func TooManyRequests(code string, retryAfterSec int) *Error {
return &Error{Status: http.StatusTooManyRequests, Code: code, Detail: "rate limited", RetryAfter: retryAfterSec}
}