2022-05-02 08:19:37 -07:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2023-09-20 06:15:43 -07:00
|
|
|
"context"
|
2022-05-02 08:19:37 -07:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
|
2023-06-03 07:18:47 -07:00
|
|
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
2023-09-20 06:15:43 -07:00
|
|
|
"emperror.dev/errors"
|
2023-09-19 17:39:14 -07:00
|
|
|
"github.com/getsentry/sentry-go"
|
|
|
|
"github.com/go-chi/chi/v5"
|
2022-05-02 08:19:37 -07:00
|
|
|
"github.com/go-chi/render"
|
|
|
|
)
|
|
|
|
|
|
|
|
// WrapHandler wraps a modified http.HandlerFunc into a stdlib-compatible one.
|
|
|
|
// The inner HandlerFunc additionally returns an error.
|
|
|
|
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2023-09-19 18:40:07 -07:00
|
|
|
hub := sentry.GetHubFromContext(r.Context())
|
|
|
|
if hub == nil {
|
|
|
|
hub = sentry.CurrentHub().Clone()
|
|
|
|
}
|
2023-09-19 17:39:14 -07:00
|
|
|
|
2022-05-02 08:19:37 -07:00
|
|
|
err := hn(w, r)
|
|
|
|
if err != nil {
|
|
|
|
// if the function returned an API error, just render that verbatim
|
|
|
|
// we can assume that it also logged the error (if that was needed)
|
|
|
|
if apiErr, ok := err.(APIError); ok {
|
|
|
|
apiErr.prepare()
|
|
|
|
|
|
|
|
render.Status(r, apiErr.Status)
|
|
|
|
render.JSON(w, r, apiErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-19 17:39:14 -07:00
|
|
|
rctx := chi.RouteContext(r.Context())
|
|
|
|
hub.ConfigureScope(func(scope *sentry.Scope) {
|
|
|
|
scope.SetTag("method", rctx.RouteMethod)
|
|
|
|
scope.SetTag("path", rctx.RoutePattern())
|
|
|
|
})
|
|
|
|
|
2023-09-20 06:15:43 -07:00
|
|
|
var eventID *sentry.EventID = nil
|
|
|
|
if isExpectedError(err) {
|
|
|
|
log.Infof("expected error in handler for %v %v, ignoring", rctx.RouteMethod, rctx.RoutePattern())
|
|
|
|
} else {
|
|
|
|
log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err)
|
|
|
|
eventID = hub.CaptureException(err)
|
|
|
|
}
|
2023-09-19 17:39:14 -07:00
|
|
|
apiErr := APIError{ID: eventID, Code: ErrInternalServerError}
|
2022-05-02 08:19:37 -07:00
|
|
|
apiErr.prepare()
|
|
|
|
|
|
|
|
render.Status(r, apiErr.Status)
|
|
|
|
render.JSON(w, r, apiErr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-20 06:15:43 -07:00
|
|
|
func isExpectedError(err error) bool {
|
|
|
|
return errors.Is(err, context.Canceled)
|
|
|
|
}
|
|
|
|
|
2022-05-02 08:19:37 -07:00
|
|
|
// APIError is an object returned by the API when an error occurs.
|
|
|
|
// It implements the error interface and can be returned by handlers.
|
|
|
|
type APIError struct {
|
2023-09-19 17:39:14 -07:00
|
|
|
Code int `json:"code"`
|
|
|
|
ID *sentry.EventID `json:"id,omitempty"`
|
|
|
|
Message string `json:"message,omitempty"`
|
|
|
|
Details string `json:"details,omitempty"`
|
2022-05-02 08:19:37 -07:00
|
|
|
|
2022-05-25 15:41:06 -07:00
|
|
|
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
|
|
|
|
|
2022-05-17 13:35:26 -07:00
|
|
|
// Status is set as the HTTP status code.
|
2022-05-02 08:19:37 -07:00
|
|
|
Status int `json:"-"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e APIError) Error() string {
|
2023-03-11 07:49:07 -08:00
|
|
|
if e.Message == "" {
|
|
|
|
e.Message = errCodeMessages[e.Code]
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Details != "" {
|
|
|
|
return fmt.Sprintf("%s (code: %d) (%s)", e.Message, e.Code, e.Details)
|
|
|
|
}
|
|
|
|
|
2022-05-02 08:19:37 -07:00
|
|
|
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *APIError) prepare() {
|
|
|
|
if e.Status == 0 {
|
|
|
|
e.Status = errCodeStatuses[e.Code]
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Message == "" {
|
|
|
|
e.Message = errCodeMessages[e.Code]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error code constants
|
|
|
|
const (
|
2022-05-04 07:27:16 -07:00
|
|
|
ErrBadRequest = 400
|
|
|
|
ErrForbidden = 403
|
2022-05-17 13:35:26 -07:00
|
|
|
ErrNotFound = 404
|
|
|
|
ErrMethodNotAllowed = 405
|
2022-05-25 15:41:06 -07:00
|
|
|
ErrTooManyRequests = 429
|
2022-05-02 08:19:37 -07:00
|
|
|
ErrInternalServerError = 500 // catch-all code for unknown errors
|
2022-05-04 07:27:16 -07:00
|
|
|
|
|
|
|
// Login/authorize error codes
|
2023-03-16 03:43:25 -07:00
|
|
|
ErrInvalidState = 1001
|
|
|
|
ErrInvalidOAuthCode = 1002
|
|
|
|
ErrInvalidToken = 1003 // a token was supplied, but it is invalid
|
|
|
|
ErrInviteRequired = 1004
|
|
|
|
ErrInvalidTicket = 1005 // invalid signup ticket
|
|
|
|
ErrInvalidUsername = 1006 // invalid username (when signing up)
|
|
|
|
ErrUsernameTaken = 1007 // username taken (when signing up)
|
|
|
|
ErrInvitesDisabled = 1008 // invites are disabled (unneeded)
|
|
|
|
ErrInviteLimitReached = 1009 // invite limit reached (when creating invites)
|
|
|
|
ErrInviteAlreadyUsed = 1010 // invite already used (when signing up)
|
|
|
|
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
|
|
|
|
ErrRecentExport = 1012 // latest export is too recent
|
|
|
|
ErrUnsupportedInstance = 1013 // unsupported fediverse software
|
2023-03-18 07:19:53 -07:00
|
|
|
ErrAlreadyLinked = 1014 // user already has linked account of the same type
|
2023-03-18 08:54:31 -07:00
|
|
|
ErrNotLinked = 1015 // user already doesn't have a linked account
|
2023-03-18 15:04:50 -07:00
|
|
|
ErrLastProvider = 1016 // unlinking provider would leave account with no authentication method
|
2023-04-24 07:51:55 -07:00
|
|
|
ErrInvalidCaptcha = 1017 // invalid or missing captcha response
|
2022-05-04 07:27:16 -07:00
|
|
|
|
|
|
|
// User-related error codes
|
2023-06-02 18:06:26 -07:00
|
|
|
ErrUserNotFound = 2001
|
|
|
|
ErrMemberListPrivate = 2002
|
|
|
|
ErrFlagLimitReached = 2003
|
|
|
|
ErrRerollingTooQuickly = 2004
|
2022-09-20 04:02:48 -07:00
|
|
|
|
|
|
|
// Member-related error codes
|
|
|
|
ErrMemberNotFound = 3001
|
|
|
|
ErrMemberLimitReached = 3002
|
2022-11-20 12:09:29 -08:00
|
|
|
ErrMemberNameInUse = 3003
|
2022-11-21 08:01:51 -08:00
|
|
|
ErrNotOwnMember = 3004
|
2022-09-20 04:02:48 -07:00
|
|
|
|
|
|
|
// General request error codes
|
2023-03-08 01:32:18 -08:00
|
|
|
ErrRequestTooBig = 4001
|
|
|
|
ErrMissingPermissions = 4002
|
2023-03-23 06:54:43 -07:00
|
|
|
|
|
|
|
// Moderation related error codes
|
|
|
|
ErrReportAlreadyHandled = 5001
|
|
|
|
ErrNotSelfDelete = 5002
|
2022-05-02 08:19:37 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
var errCodeMessages = map[int]string{
|
2022-05-04 07:27:16 -07:00
|
|
|
ErrBadRequest: "Bad request",
|
|
|
|
ErrForbidden: "Forbidden",
|
2022-05-02 08:19:37 -07:00
|
|
|
ErrInternalServerError: "Internal server error",
|
2022-05-17 13:35:26 -07:00
|
|
|
ErrNotFound: "Not found",
|
2022-05-25 15:41:06 -07:00
|
|
|
ErrTooManyRequests: "Rate limit reached",
|
2022-05-17 13:35:26 -07:00
|
|
|
ErrMethodNotAllowed: "Method not allowed",
|
2022-05-04 07:27:16 -07:00
|
|
|
|
2023-03-16 03:43:25 -07:00
|
|
|
ErrInvalidState: "Invalid OAuth state",
|
|
|
|
ErrInvalidOAuthCode: "Invalid OAuth code",
|
|
|
|
ErrInvalidToken: "Supplied token was invalid",
|
|
|
|
ErrInviteRequired: "A valid invite code is required",
|
|
|
|
ErrInvalidTicket: "Invalid signup ticket",
|
|
|
|
ErrInvalidUsername: "Invalid username",
|
|
|
|
ErrUsernameTaken: "Username is already taken",
|
|
|
|
ErrInvitesDisabled: "Invites are disabled",
|
|
|
|
ErrInviteLimitReached: "Your account has reached the invite limit",
|
|
|
|
ErrInviteAlreadyUsed: "That invite code has already been used",
|
|
|
|
ErrDeletionPending: "Your account is pending deletion",
|
|
|
|
ErrRecentExport: "Your latest data export is less than 1 day old",
|
|
|
|
ErrUnsupportedInstance: "Unsupported instance software",
|
2023-03-18 07:19:53 -07:00
|
|
|
ErrAlreadyLinked: "Your account is already linked to an account of this type",
|
2023-03-18 08:54:31 -07:00
|
|
|
ErrNotLinked: "Your account is already not linked to an account of this type",
|
2023-03-18 15:04:50 -07:00
|
|
|
ErrLastProvider: "This is your account's only authentication provider",
|
2023-04-24 07:51:55 -07:00
|
|
|
ErrInvalidCaptcha: "Invalid or missing captcha response",
|
2022-05-04 07:27:16 -07:00
|
|
|
|
2023-06-02 18:06:26 -07:00
|
|
|
ErrUserNotFound: "User not found",
|
|
|
|
ErrMemberListPrivate: "This user's member list is private",
|
|
|
|
ErrFlagLimitReached: "Maximum number of pride flags reached",
|
|
|
|
ErrRerollingTooQuickly: "You can only reroll one short ID per hour.",
|
2022-09-20 04:02:48 -07:00
|
|
|
|
|
|
|
ErrMemberNotFound: "Member not found",
|
|
|
|
ErrMemberLimitReached: "Member limit reached",
|
2022-11-20 12:09:29 -08:00
|
|
|
ErrMemberNameInUse: "Member name already in use",
|
2022-11-21 08:01:51 -08:00
|
|
|
ErrNotOwnMember: "Not your member",
|
2022-09-20 04:02:48 -07:00
|
|
|
|
2023-03-08 01:32:18 -08:00
|
|
|
ErrRequestTooBig: "Request too big (max 2 MB)",
|
|
|
|
ErrMissingPermissions: "Your account or current token is missing required permissions for this action",
|
2023-03-23 06:54:43 -07:00
|
|
|
|
|
|
|
ErrReportAlreadyHandled: "Report has already been resolved",
|
|
|
|
ErrNotSelfDelete: "Cannot cancel deletion for an account deleted by a moderator",
|
2022-05-02 08:19:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
var errCodeStatuses = map[int]int{
|
2022-05-04 07:27:16 -07:00
|
|
|
ErrBadRequest: http.StatusBadRequest,
|
|
|
|
ErrForbidden: http.StatusForbidden,
|
2022-05-02 08:19:37 -07:00
|
|
|
ErrInternalServerError: http.StatusInternalServerError,
|
2022-05-17 13:35:26 -07:00
|
|
|
ErrNotFound: http.StatusNotFound,
|
2022-05-25 15:41:06 -07:00
|
|
|
ErrTooManyRequests: http.StatusTooManyRequests,
|
2022-05-17 13:35:26 -07:00
|
|
|
ErrMethodNotAllowed: http.StatusMethodNotAllowed,
|
2022-05-04 07:27:16 -07:00
|
|
|
|
2023-03-16 03:43:25 -07:00
|
|
|
ErrInvalidState: http.StatusBadRequest,
|
|
|
|
ErrInvalidOAuthCode: http.StatusForbidden,
|
|
|
|
ErrInvalidToken: http.StatusUnauthorized,
|
|
|
|
ErrInviteRequired: http.StatusBadRequest,
|
|
|
|
ErrInvalidTicket: http.StatusBadRequest,
|
|
|
|
ErrInvalidUsername: http.StatusBadRequest,
|
|
|
|
ErrUsernameTaken: http.StatusBadRequest,
|
|
|
|
ErrInvitesDisabled: http.StatusForbidden,
|
|
|
|
ErrInviteLimitReached: http.StatusForbidden,
|
|
|
|
ErrInviteAlreadyUsed: http.StatusBadRequest,
|
|
|
|
ErrDeletionPending: http.StatusBadRequest,
|
|
|
|
ErrRecentExport: http.StatusBadRequest,
|
|
|
|
ErrUnsupportedInstance: http.StatusBadRequest,
|
2023-03-18 07:19:53 -07:00
|
|
|
ErrAlreadyLinked: http.StatusBadRequest,
|
2023-03-18 08:54:31 -07:00
|
|
|
ErrNotLinked: http.StatusBadRequest,
|
2023-03-18 15:04:50 -07:00
|
|
|
ErrLastProvider: http.StatusBadRequest,
|
2023-04-24 07:51:55 -07:00
|
|
|
ErrInvalidCaptcha: http.StatusBadRequest,
|
2022-05-04 07:27:16 -07:00
|
|
|
|
2023-06-02 18:06:26 -07:00
|
|
|
ErrUserNotFound: http.StatusNotFound,
|
|
|
|
ErrMemberListPrivate: http.StatusForbidden,
|
|
|
|
ErrFlagLimitReached: http.StatusBadRequest,
|
|
|
|
ErrRerollingTooQuickly: http.StatusForbidden,
|
2022-09-20 04:02:48 -07:00
|
|
|
|
|
|
|
ErrMemberNotFound: http.StatusNotFound,
|
|
|
|
ErrMemberLimitReached: http.StatusBadRequest,
|
2022-11-20 12:09:29 -08:00
|
|
|
ErrMemberNameInUse: http.StatusBadRequest,
|
2022-11-21 08:01:51 -08:00
|
|
|
ErrNotOwnMember: http.StatusForbidden,
|
2022-09-20 04:02:48 -07:00
|
|
|
|
2023-03-08 01:32:18 -08:00
|
|
|
ErrRequestTooBig: http.StatusBadRequest,
|
|
|
|
ErrMissingPermissions: http.StatusForbidden,
|
2023-03-23 06:54:43 -07:00
|
|
|
|
|
|
|
ErrReportAlreadyHandled: http.StatusBadRequest,
|
|
|
|
ErrNotSelfDelete: http.StatusForbidden,
|
2022-05-02 08:19:37 -07:00
|
|
|
}
|