77 lines
2.7 KiB
Go
77 lines
2.7 KiB
Go
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}
|
|
}
|