first commit
This commit is contained in:
76
go-api/internal/domain/errors.go
Normal file
76
go-api/internal/domain/errors.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error is an API-level failure carrying the contract's error code.
|
||||
// See api-contract.md §5.
|
||||
type Error struct {
|
||||
Code string
|
||||
Message string
|
||||
Details map[string]string
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string { return e.Message }
|
||||
func (e *Error) Unwrap() error { return e.cause }
|
||||
|
||||
// NotFound reproduces store.js's thrown message verbatim: "<Entity> <id> not
|
||||
// found", using the frontend's entity name rather than the table name.
|
||||
func NotFound(entity, id string) *Error {
|
||||
return &Error{Code: "not_found", Message: fmt.Sprintf("%s %s not found", entity, id)}
|
||||
}
|
||||
|
||||
func Invalid(msg string) *Error {
|
||||
return &Error{Code: "invalid_query", Message: msg}
|
||||
}
|
||||
|
||||
func Validation(msg string, details map[string]string) *Error {
|
||||
if details == nil {
|
||||
details = map[string]string{}
|
||||
}
|
||||
return &Error{Code: "validation_failed", Message: msg, Details: details}
|
||||
}
|
||||
|
||||
func Conflict(msg string) *Error {
|
||||
return &Error{Code: "conflict", Message: msg}
|
||||
}
|
||||
|
||||
// Unauthenticated is every "you are not signed in" answer: no cookie, an
|
||||
// unknown token, an expired session, a suspended user, a wrong password, an
|
||||
// email that does not exist.
|
||||
//
|
||||
// One constructor for all of them, deliberately. The distinctions matter in the
|
||||
// server log and must not reach the client: which of those it was tells an
|
||||
// attacker whether an address is registered, whether an account is suspended,
|
||||
// or whether a guessed token was ever real.
|
||||
func Unauthenticated() *Error {
|
||||
return &Error{Code: "unauthorized", Message: "authentication required"}
|
||||
}
|
||||
|
||||
// Forbidden is the answer to an authenticated caller whose role does not permit
|
||||
// the operation.
|
||||
//
|
||||
// Distinct from Unauthenticated: 401 means "I do not know who you are", 403
|
||||
// means "I know exactly who you are and the answer is still no". Conflating
|
||||
// them makes a client retry a login that will not help.
|
||||
//
|
||||
// The message names neither the role the caller has nor the roles that would
|
||||
// have worked. That is not secrecy for its own sake — it is that an endpoint
|
||||
// which answers "employers only" to a talent user is an endpoint that maps the
|
||||
// organization's privilege structure for anyone who asks.
|
||||
//
|
||||
// Note what does NOT come through here: a row belonging to another
|
||||
// organization, or to another person, is not forbidden — it is absent. Those
|
||||
// answer 404 by way of a SQL predicate, so existence never leaks.
|
||||
func Forbidden() *Error {
|
||||
return &Error{Code: "forbidden", Message: "you do not have access to this operation"}
|
||||
}
|
||||
|
||||
// RateLimited is the answer to too many failed sign-in attempts.
|
||||
func RateLimited(msg string) *Error {
|
||||
return &Error{Code: "rate_limited", Message: msg}
|
||||
}
|
||||
|
||||
func Internal(err error) *Error {
|
||||
return &Error{Code: "internal", Message: "internal error", cause: err}
|
||||
}
|
||||
Reference in New Issue
Block a user