Files
vikingowl 0997d4befa feat(auth): D1 non-breaking security foundations
- CORS: rewrite middleware with Vary: Origin, regex origin patterns,
  startup validation, and prod boot-fail on empty allowlist; shared
  CORSConfig exported for CSRF reuse
- CSRF: new Origin/Referer check middleware sharing CORS allowlist;
  Bearer-token clients exempt; mounts globally after CORS
- Argon2id: new password package with PHC format, bcrypt dispatch, and
  NeedsRehash; lazy upgrade on login in auth service
- Rate limiting: add RateLimitByKey with custom key function; apply
  per-route limits to /auth/login, /refresh, /2fa/verify,
  /auth/magic-link, and /auth/password
- apierror: add CSRFMismatch and RefreshReuse error constructors
- Migrations: 000027 (session model schema columns for D2/D3),
  000028 (TOTP secret_v2 column + totp_backup_codes table)
- cmd/totp-encrypt: one-shot job to encrypt existing TOTP secrets
2026-04-26 11:54:37 +02:00

98 lines
2.4 KiB
Go

package apierror
import (
"fmt"
"net/http"
)
type Error struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
}
func (e *Error) Error() string {
return fmt.Sprintf("[%d] %s: %s", e.Status, e.Code, e.Message)
}
type Response struct {
Error *ErrorBody `json:"error"`
}
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
}
func NewResponse(e *Error) Response {
return Response{
Error: &ErrorBody{
Code: e.Code,
Message: e.Message,
},
}
}
func BadRequest(code, message string) *Error {
return &Error{Status: http.StatusBadRequest, Code: code, Message: message}
}
func Unauthorized(message string) *Error {
return &Error{Status: http.StatusUnauthorized, Code: "unauthorized", Message: message}
}
func Forbidden(message string) *Error {
return &Error{Status: http.StatusForbidden, Code: "forbidden", Message: message}
}
func NotFound(resource string) *Error {
return &Error{
Status: http.StatusNotFound,
Code: "not_found",
Message: fmt.Sprintf("%s not found", resource),
}
}
func Conflict(message string) *Error {
return &Error{Status: http.StatusConflict, Code: "conflict", Message: message}
}
// TooManyRequests accepts an optional custom message. Callers that want the
// default — "too many requests, please try again later" — pass no arguments;
// callers with context (e.g. "retry in ~60s") pass a single string.
func TooManyRequests(message ...string) *Error {
msg := "too many requests, please try again later"
if len(message) > 0 && message[0] != "" {
msg = message[0]
}
return &Error{
Status: http.StatusTooManyRequests,
Code: "rate_limited",
Message: msg,
}
}
func Internal(message string) *Error {
return &Error{
Status: http.StatusInternalServerError,
Code: "internal_error",
Message: message,
}
}
func Validation(message string) *Error {
return &Error{Status: http.StatusUnprocessableEntity, Code: "validation_error", Message: message}
}
func Gone(message string) *Error {
return &Error{Status: http.StatusGone, Code: "gone", Message: message}
}
func CSRFMismatch() *Error {
return &Error{Status: http.StatusForbidden, Code: "auth.csrf_mismatch", Message: "CSRF validation failed"}
}
func RefreshReuse() *Error {
return &Error{Status: http.StatusUnauthorized, Code: "auth.refresh_reuse_detected", Message: "session token reuse detected"}
}