From 4145944b1b7b2b38a7102cbebfab67f0ceb50adf Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Apr 2026 21:34:17 +0000 Subject: [PATCH] Allow arbitrary responses from form-encoded POST Useful for returning full objects --- api/handler.go | 24 ++++++++++++++---------- resource/avatar.go | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/api/handler.go b/api/handler.go index 0bca11c5..acf49e0b 100644 --- a/api/handler.go +++ b/api/handler.go @@ -126,23 +126,29 @@ func authenticatedHandlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.Han } type handlerFunctionPost[ReqType any] func(context.Context, *http.Request, ReqType) (string, *nhttp.ErrorWithStatus) -type handlerFunctionPostAuthenticated[ReqType any] func(context.Context, *http.Request, platform.User, ReqType) (string, *nhttp.ErrorWithStatus) +type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus) -func authenticatedHandlerJSONPost[ReqType any](f handlerFunctionPostAuthenticated[ReqType]) http.Handler { +func authenticatedHandlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPostAuthenticated[RequestType, ResponseType]) http.Handler { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { w.Header().Set("Content-Type", "application/json") - req, e := parseRequest[ReqType](r) + req, e := parseRequest[RequestType](r) if e != nil { serializeError(w, e) return } ctx := r.Context() - path, e := f(ctx, r, u, *req) + resp, e := f(ctx, r, u, *req) if e != nil { serializeError(w, e) return } - http.Redirect(w, r, path, http.StatusFound) + body, err := json.Marshal(resp) + if err != nil { + log.Error().Err(err).Msg("failed to marshal json") + http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError) + return + } + w.Write(body) }) } @@ -214,7 +220,7 @@ type postMultipartResponse struct { URI string `json:"uri"` } -func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file.Upload], collection file.Collection) http.Handler { +func authenticatedHandlerPostMultipart[ResponseType any](f handlerFunctionPostAuthenticated[[]file.Upload, ResponseType], collection file.Collection) http.Handler { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { err := r.ParseMultipartForm(32 << 10) // 32 MB buffer if err != nil { @@ -235,14 +241,12 @@ func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file } */ ctx := r.Context() - path, e := f(ctx, r, u, uploads) + resp, e := f(ctx, r, u, uploads) if e != nil { http.Error(w, e.Error(), e.Status) return } - body, err := json.Marshal(postMultipartResponse{ - URI: path, - }) + body, err := json.Marshal(resp) if err != nil { log.Error().Err(err).Msg("failed to marshal json") http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError) diff --git a/resource/avatar.go b/resource/avatar.go index 2fcefd6d..dc1f82ac 100644 --- a/resource/avatar.go +++ b/resource/avatar.go @@ -2,7 +2,6 @@ package resource import ( "context" - "fmt" "net/http" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" @@ -21,6 +20,9 @@ func Avatar(r *router) *avatarR { type avatarR struct { router *router } +type avatar struct { + URI string `json:"uri"` +} func (res *avatarR) ByIDGet(ctx context.Context, r *http.Request, u platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus) { vars := mux.Vars(r) @@ -31,17 +33,23 @@ func (res *avatarR) ByIDGet(ctx context.Context, r *http.Request, u platform.Use } return file.CollectionAvatar, uid, nil } -func (res *avatarR) Create(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { +func (res *avatarR) Create(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (*avatar, *nhttp.ErrorWithStatus) { if len(uploads) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") } if len(uploads) != 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") } upload := uploads[0] err := platform.AvatarCreate(r.Context(), u, upload) if err != nil { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Create avatar: %w", err) + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Create avatar: %w", err) } - return fmt.Sprintf("/avatar/%s", upload.UUID.String()), nil + uri, err := res.router.UUIDToURI("avatar.ByIDGet", &upload.UUID) + if err != nil { + return nil, nhttp.NewError("create uri: %w", err) + } + return &avatar{ + URI: *uri, + }, nil }