Files
krow_backend/go-api/internal/domain/errors.go
2026-08-24 13:06:29 +05:30

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}
}