feat: option A — visibility gating + author submission system
Some checks failed
Release / Test backend (push) Successful in 1m1s
Release / Check ui (push) Successful in 1m0s
Release / Docker (push) Successful in 11m19s
Release / Deploy to prod (push) Successful in 2m10s
Release / Deploy to homelab (push) Failing after 7s
Release / Gitea Release (push) Successful in 2m25s
Some checks failed
Release / Test backend (push) Successful in 1m1s
Release / Check ui (push) Successful in 1m0s
Release / Docker (push) Successful in 11m19s
Release / Deploy to prod (push) Successful in 2m10s
Release / Deploy to homelab (push) Failing after 7s
Release / Gitea Release (push) Successful in 2m25s
Content visibility:
- Add `visibility` field to books ("public" | "admin_only"); new migration
backfills all existing scraped books to admin_only
- Meilisearch: add visibility as filterable attribute; catalogue/search
endpoints filter to public-only for non-admin requests
- Admin users identified by bearer token bypass the filter and see all books
- All PocketBase discovery queries (trending, recommended, recently-updated,
audio shelf, discover, subscription feed) now filter to visibility=public
- New scraped books default to admin_only; WriteMetadata preserves existing
visibility on PATCH (never overwrites)
Author submission:
- POST /api/admin/books/submit — creates a public book with submitted_by
- PATCH /api/admin/books/{slug}/publish / unpublish — toggle visibility
- SvelteKit proxies: /api/admin/books/[slug]/publish|unpublish
- /api/books/[slug] endpoint for admin book lookup
Frontend:
- backendFetchAdmin() helper sends admin token on any path
- Catalogue server load uses admin fetch when user is admin
- /submit page: author submission form with genre picker and rights assertion
- "Publish" nav link shown to all logged-in users
- Admin catalogue-tools: visibility management panel (load book by slug, toggle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,9 @@ type CatalogueQuery struct {
|
||||
Sort string // sort field: "popular", "new", "update", "top-rated", "rank", ""
|
||||
Page int // 1-indexed
|
||||
Limit int // items per page, default 20
|
||||
// AdminAll disables the visibility filter so admin users see all non-archived
|
||||
// books including those marked admin_only.
|
||||
AdminAll bool
|
||||
}
|
||||
|
||||
// FacetResult holds the available filter values discovered from the index.
|
||||
@@ -103,7 +106,7 @@ func Configure(host, apiKey string) error {
|
||||
return fmt.Errorf("meili: update searchable attributes: %w", err)
|
||||
}
|
||||
|
||||
filterable := []interface{}{"status", "genres", "archived"}
|
||||
filterable := []interface{}{"status", "genres", "archived", "visibility"}
|
||||
if _, err := idx.UpdateFilterableAttributes(&filterable); err != nil {
|
||||
return fmt.Errorf("meili: update filterable attributes: %w", err)
|
||||
}
|
||||
@@ -135,6 +138,9 @@ type bookDoc struct {
|
||||
// Archived is true when the book has been soft-deleted by an admin.
|
||||
// Used as a filter to exclude archived books from all search results.
|
||||
Archived bool `json:"archived"`
|
||||
// Visibility is "public" or "admin_only". Only public books are shown to
|
||||
// non-admin users. Empty string is treated as admin_only for safety.
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
|
||||
func toDoc(b domain.BookMeta) bookDoc {
|
||||
@@ -152,6 +158,7 @@ func toDoc(b domain.BookMeta) bookDoc {
|
||||
Rating: b.Rating,
|
||||
MetaUpdated: b.MetaUpdated,
|
||||
Archived: b.Archived,
|
||||
Visibility: b.Visibility,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +177,7 @@ func fromDoc(d bookDoc) domain.BookMeta {
|
||||
Rating: d.Rating,
|
||||
MetaUpdated: d.MetaUpdated,
|
||||
Archived: d.Archived,
|
||||
Visibility: d.Visibility,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +218,7 @@ func (c *MeiliClient) Search(_ context.Context, query string, limit int) ([]doma
|
||||
}
|
||||
res, err := c.idx.Search(query, &meilisearch.SearchRequest{
|
||||
Limit: int64(limit),
|
||||
Filter: "archived = false",
|
||||
Filter: `archived = false AND visibility = "public"`,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meili: search %q: %w", query, err)
|
||||
@@ -251,8 +259,11 @@ func (c *MeiliClient) Catalogue(_ context.Context, q CatalogueQuery) ([]domain.B
|
||||
Facets: []string{"genres", "status"},
|
||||
}
|
||||
|
||||
// Build filter — always exclude archived books
|
||||
// Build filter — always exclude archived books; restrict to public unless admin.
|
||||
filters := []string{"archived = false"}
|
||||
if !q.AdminAll {
|
||||
filters = append(filters, `visibility = "public"`)
|
||||
}
|
||||
if q.Genre != "" && q.Genre != "all" {
|
||||
filters = append(filters, fmt.Sprintf("genres = %q", q.Genre))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user