Documentation
¶
Index ¶
- type ContentHandler
- func (h *ContentHandler) Create(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) Publish(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) Unpublish(w http.ResponseWriter, r *http.Request)
- func (h *ContentHandler) Update(w http.ResponseWriter, r *http.Request)
- type ContentProjection
- type ContentRequest
- type ContentResponse
- type ContentService
- type MediaHandler
- type MediaMetadata
- type MediaProjection
- type MediaResponse
- type MediaService
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ContentHandler ¶
type ContentHandler struct {
// contains filtered or unexported fields
}
ContentHandler exposes the Bearer-authenticated agent content endpoints. It reuses the existing ContentService (which already validates custom fields and fires plugin hooks) — it never duplicates that logic.
func NewContentHandler ¶
func NewContentHandler(s ContentService, logger *util.Logger) *ContentHandler
NewContentHandler constructs a ContentHandler backed by the given content service. A nil logger degrades to a discard sink so the handler is safe to construct in any context (mirrors NewAPIKeyHandler/NewAPIKeyAuthMiddleware).
func (*ContentHandler) Create ¶
func (h *ContentHandler) Create(w http.ResponseWriter, r *http.Request)
Create handles POST /api/v1/content. It maps the streamlined agent payload onto a contentdomain.CreateContentRequest (reusing the same validation + plugin hook path as the admin surface) and returns the created content in the envelope.
func (*ContentHandler) Delete ¶
func (h *ContentHandler) Delete(w http.ResponseWriter, r *http.Request)
Delete handles DELETE /api/v1/content/{id}. It pre-fetches for the ownership check (404 on not-found or not-owned-per-role) then delegates to the existing ContentService.DeleteContent. A successful delete returns 204 No Content; a subsequent GET returns 404 (the service hard-deletes for Admins and scoped-deletes otherwise).
func (*ContentHandler) Get ¶
func (h *ContentHandler) Get(w http.ResponseWriter, r *http.Request)
Get handles GET /api/v1/content/{id}. It returns the content in the envelope, or 404 NOT_FOUND when it does not exist OR the caller is not allowed to see it.
Visibility scoping (review fix for the GET IDOR): the underlying GetByID returns content regardless of owner/status, so the handler enforces "published OR owned by the requesting user" and otherwise returns NOT_FOUND (never FORBIDDEN, so existence is not disclosed to unauthorized callers). This mirrors the ownership pattern the admin Update/DeleteContent paths use (existing.UserID != userID).
func (*ContentHandler) List ¶
func (h *ContentHandler) List(w http.ResponseWriter, r *http.Request)
List handles GET /api/v1/content. It returns the caller's own content (any status — drafts + published) in newest-first order using opaque keyset (cursor) pagination, each item projected via the ContentProjection whitelist. hasMore/nextCursor are computed by requesting limit+1 rows; nextCursor is the opaque token of the last item on the current page and is only present when there is another page.
Optional filters: ?tag=foo&tag=bar (AND-of-tags), ?language=en, ?status=draft|published, ?post_type=post, ?author=alice (admin only — non-admins get 403), ?search=foo (min 2 chars). All filters AND with the cursor pagination. The agent v1 surface scopes to the caller's own content; the author filter is meaningful only for admins because the agent handler never expands the userID scope (admin still sees their own content here — use the admin surface for cross-user listings).
func (*ContentHandler) Publish ¶
func (h *ContentHandler) Publish(w http.ResponseWriter, r *http.Request)
Publish handles POST /api/v1/content/{id}/publish. It delegates to ContentService.Publish so ownership, the draft→published transition, SEO auto-generation, and the AfterPublish hook fire through the same domain path the Update endpoint uses. The endpoint accepts an empty body and is idempotent: publishing an already-published post returns 200 with the current projection, fires no hook, runs no SEO regen.
A non-admin caller must be the owner; otherwise 404 (never 403, so existence is not disclosed). An admin can publish another user's content. Validation and persistence errors map to the same error envelope the other endpoints use.
func (*ContentHandler) Unpublish ¶
func (h *ContentHandler) Unpublish(w http.ResponseWriter, r *http.Request)
Unpublish handles POST /api/v1/content/{id}/unpublish. It delegates to ContentService.Unpublish so the status flip persists through the same domain path. The endpoint accepts an empty body and is idempotent: unpublishing an already-draft post returns 200 with the current projection, fires no hook, runs no SEO regen. Unpublish never fires the AfterPublish hook (the hook is wired to the draft→published edge only).
Same ownership/role contract as Publish: 404 (not 403) for non-admin non-owners; admins can unpublish anyone's content.
func (*ContentHandler) Update ¶
func (h *ContentHandler) Update(w http.ResponseWriter, r *http.Request)
Update handles PUT /api/v1/content/{id}. It pre-fetches the existing content for an ownership check (404 NOT_FOUND when the caller is neither owner nor Admin — existence is never disclosed) and then delegates to the existing ContentService.Update so hooks + custom-field validation run through the same domain path the admin uses. format=tiptap stores the body unchanged; markdown is rejected (Story 2.4).
v1 settable on update: title, body, format, postType, customFields, isPublished, tags, language. Server-managed (preserved from the existing item): SEO metadata (metaDescription, ogTitle, ogDescription), allowComments, translationGroupId.
type ContentProjection ¶
type ContentProjection struct {
ID int `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Body string `json:"body"`
Status string `json:"status"`
PostType string `json:"postType,omitempty"`
Language string `json:"language,omitempty"`
Tags []string `json:"tags,omitempty"`
CustomFields map[string]any `json:"customFields,omitempty"`
Author string `json:"author,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
ContentProjection is the public, whitelisted view of a content item returned by the agent create/get endpoints. It deliberately excludes internal/admin-only fields the raw contentdomain.Content carries (the numeric owner id, updatedBy/updatedByUsername, translationGroupId, SEO metadata, allowComments) so the programmatic API surface does not over-disclose to integrators or — via IDOR — to other tenants.
type ContentRequest ¶
type ContentRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Format string `json:"format,omitempty"`
PostType string `json:"postType,omitempty"`
Slug string `json:"slug,omitempty"`
CustomFields map[string]any `json:"customFields,omitempty"`
IsPublished bool `json:"isPublished,omitempty"`
Tags []string `json:"tags,omitempty"`
Language string `json:"language,omitempty"`
}
ContentRequest is the agent authoring payload for POST /api/v1/content. It maps onto the existing contentdomain.CreateContentRequest (see the mapping table in the story Dev Notes). It is intentionally separate from the admin handler's CreateContentRequest so the agent contract can evolve independently.
Field notes:
- Body is the canonical content. For Format "tiptap" (default) it is the Tiptap JSON string, stored unchanged. The service validates/sanitizes it.
- Format defaults to "tiptap" when omitted. "markdown" is reserved for Story 2.4 (Markdown→Tiptap conversion) and is rejected with VALIDATION_ERROR here.
- Slug is accepted for API stability but NOT honored in Story 2.1: the content service auto-generates the slug from the title. See Completion Notes.
- IsPublished true maps to StatusPublished; false/omitted maps to StatusDraft.
- Tags are normalized via contentdomain.ValidateTags; an invalid tag returns VALIDATION_ERROR from the server.
- Language must be in the server's configured languages (config.toml [languages] list); an unknown code returns VALIDATION_ERROR (ErrInvalidLanguage). The CLI cannot pre-validate this and forwards the value as-given so the server is the single source of truth.
type ContentResponse ¶
type ContentResponse struct {
Content ContentProjection `json:"content"`
}
ContentResponse wraps a projected content item for the agent create/get responses. Envelope body: {"data":{"content":{...}}}.
func NewContentResponse ¶
func NewContentResponse(c *contentdomain.Content) ContentResponse
NewContentResponse builds a ContentResponse projecting the given content entity to its public fields. A nil entity yields an empty projection (defensive — the service contract returns a non-nil content on success).
type ContentService ¶
type ContentService interface {
Create(ctx context.Context, userID int, req contentdomain.CreateContentRequest) (*contentdomain.Content, error)
GetByID(ctx context.Context, id int) (*contentdomain.Content, error)
ListByCursor(ctx context.Context, userID int, limit int, beforeID int, filters contentdomain.ContentFilters) ([]*contentdomain.Content, error)
Update(ctx context.Context, id int, userID int, role string, req contentdomain.UpdateContentRequest) (*contentdomain.Content, error)
DeleteContent(ctx context.Context, id int, userID int, role string) error
Publish(ctx context.Context, id int, userID int, role string) (*contentdomain.Content, error)
Unpublish(ctx context.Context, id int, userID int, role string) (*contentdomain.Content, error)
}
ContentService is the narrow slice of the content domain service the agent handlers depend on. *contentdomain.Service satisfies it. Declaring it locally — instead of depending on the admin handler's wider ContentServiceInterface — keeps the agent surface independent and makes the dependency explicit. It is exported so mockery can generate an exported constructor callable from the package's *_test files (CLAUDE.md mandates *_test packages, which cannot reach an unexported mock).
type MediaHandler ¶
type MediaHandler struct {
// contains filtered or unexported fields
}
MediaHandler exposes the Bearer-authenticated agent media endpoints. It reuses the existing MediaService (which already validates files, hashes, dedups, converts to WebP, and builds thumbnail variants) — it never duplicates that logic.
func NewMediaHandler ¶
func NewMediaHandler(s MediaService, logger *util.Logger) *MediaHandler
NewMediaHandler constructs a MediaHandler backed by the given media service. A nil logger degrades to a discard sink so the handler is safe to construct in any context (mirrors NewContentHandler).
func (*MediaHandler) Get ¶
func (h *MediaHandler) Get(w http.ResponseWriter, r *http.Request)
Get handles GET /api/v1/media/{id}. It returns the media in the envelope, or 404 NOT_FOUND when it does not exist OR the caller is not allowed to see it (ownership scoping: only the owner or an Admin may read a given item — existence is never disclosed, consistent with the content visibility model).
func (*MediaHandler) List ¶
func (h *MediaHandler) List(w http.ResponseWriter, r *http.Request)
List handles GET /api/v1/media. It returns the caller's own media in newest-first order using opaque keyset (cursor) pagination — the SAME contract as the content list. It reuses the agent package's cursor helpers and the response package's SuccessList/ Pagination/ListMeta types (introduced for this reuse in Story 2.2).
func (*MediaHandler) Upload ¶
func (h *MediaHandler) Upload(w http.ResponseWriter, r *http.Request)
Upload handles POST /api/v1/media. It parses a required `file` part and an optional JSON `metadata` part (carrying altText) from multipart/form-data, maps them onto a mediadomain.UploadRequest, and delegates to the existing MediaService.Upload. A missing `file` part returns 400 VALIDATION_ERROR; the service's own validation (mime/size/alt text) and dedup flow through handleMediaError.
type MediaMetadata ¶
type MediaMetadata struct {
AltText string `json:"altText,omitempty"`
}
MediaMetadata is the optional JSON `metadata` part of a media upload. altText is the only field today; it is REQUIRED by the media service (ValidateAltText) for accessibility, so agents should always send it.
type MediaProjection ¶
type MediaProjection struct {
ID int `json:"id"`
Filename string `json:"filename"`
OriginalFilename string `json:"originalFilename"`
MimeType mediadomain.MimeType `json:"mimeType"`
FileSize int64 `json:"fileSize"`
Width int `json:"width"`
Height int `json:"height"`
AltText string `json:"altText"`
IsWebP bool `json:"isWebp"`
Hash string `json:"hash"`
URL string `json:"url"`
Variants map[string]mediadomain.MediaVariant `json:"variants,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
MediaProjection is the public, whitelisted view of a media item returned by the agent media endpoints. It deliberately excludes internal/admin-only fields the raw mediadomain.Media carries (the numeric owner id, filePath, uploadedBy) so the programmatic API surface does not over-disclose — mirroring ContentProjection.
type MediaResponse ¶
type MediaResponse struct {
Media MediaProjection `json:"media"`
}
MediaResponse wraps a projected media item for the agent media responses. Envelope body: {"data":{"media":{...}}}.
func NewMediaResponse ¶
func NewMediaResponse(m *mediadomain.Media) MediaResponse
NewMediaResponse builds a MediaResponse projecting the given media entity to its public fields. A nil entity yields an empty projection (defensive — the service contract returns a non-nil media on success).
type MediaService ¶
type MediaService interface {
Upload(ctx context.Context, req mediadomain.UploadRequest) (*mediadomain.Media, error)
GetByID(ctx context.Context, id int) (*mediadomain.Media, error)
ListByCursor(ctx context.Context, userID int, limit int, beforeID int) ([]*mediadomain.Media, error)
}
MediaService is the narrow slice of the media domain service the agent handlers depend on. *mediadomain.Service satisfies it. It is exported so mockery can generate an exported constructor callable from the package's *_test files (CLAUDE.md mandates *_test packages, which cannot reach an unexported mock).