diff --git a/cmd/server/tests/device_cap_test.go b/cmd/server/tests/device_cap_test.go
index 957a6c43..e72e7eff 100644
--- a/cmd/server/tests/device_cap_test.go
+++ b/cmd/server/tests/device_cap_test.go
@@ -336,7 +336,7 @@ func createTestUserForMaxDevices(t *testing.T, ts *httptest.Server, adminToken s
}
jsonData, _ := json.Marshal(createUserPayload)
- req, _ := http.NewRequest("POST", ts.URL+"/api/auth/register", bytes.NewBuffer(jsonData))
+ req, _ := http.NewRequest("POST", ts.URL+"/api/auth/users", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
@@ -416,14 +416,19 @@ func createTestUserForMaxDevices(t *testing.T, ts *httptest.Server, adminToken s
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
- // Check if user creation was successful
- if result["user"] == nil {
+ // Admin create returns the user object at the top level; the legacy
+ // register endpoint nested it under "user". Accept both.
+ userObj, ok := result["user"].(map[string]interface{})
+ if !ok {
+ userObj = result
+ }
+ if userObj["id"] == nil {
// User creation failed for another reason
t.Logf("User creation failed, response: %+v", result)
return uuid.UUID{}
}
- userIDStr := result["user"].(map[string]interface{})["id"].(string)
+ userIDStr, _ := userObj["id"].(string)
userID, _ := uuid.Parse(userIDStr)
return userID
diff --git a/cmd/server/tests/opds_test.go b/cmd/server/tests/opds_test.go
index bba89334..551f24ef 100644
--- a/cmd/server/tests/opds_test.go
+++ b/cmd/server/tests/opds_test.go
@@ -505,7 +505,11 @@ func TestOPDSLibraryFolders(t *testing.T) {
})
t.Run("all=1 keeps the flat cross-library catalog", func(t *testing.T) {
- status, contentType, body := getFeed(t, base+"/catalog?all=1")
+ // Explicit large page: the shared test DB accumulates items, and
+ // the freshly created books sort last — without a generous
+ // per_page they fall onto a later page and the assertion reads
+ // tea leaves instead of the contract.
+ status, contentType, body := getFeed(t, base+"/catalog?all=1&per_page=200")
require.Equal(t, http.StatusOK, status)
assert.Contains(t, contentType, "kind=acquisition")
assert.Contains(t, body, "OPDS Folder Book 1")
diff --git a/cmd/server/tests/ownership_and_admin_test.go b/cmd/server/tests/ownership_and_admin_test.go
new file mode 100644
index 00000000..266b5291
--- /dev/null
+++ b/cmd/server/tests/ownership_and_admin_test.go
@@ -0,0 +1,217 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+ "time"
+
+ "bookhoard/internal/database"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5/pgtype"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+)
+
+// OwnershipAndAdminTestSuite pins the 2026-10-13 hardening: per-user
+// annotation mutations are ownership-scoped (no cross-user delete/update by
+// bare row id), and admins can create users even with self-service
+// registration disabled.
+type OwnershipAndAdminTestSuite struct {
+ suite.Suite
+ setup *TestServerSetup
+ token string
+ itemID string
+}
+
+func (s *OwnershipAndAdminTestSuite) SetupSuite() {
+ s.setup = setupTestServer(s.T())
+ s.token = s.setup.Token
+ s.itemID = s.createItemViaDB()
+ // createTestMediaItemID's t.Cleanup path can't be used at suite level
+ // (it fires after TearDownSuite has closed the server), so the item is
+ // created directly through the DB handle; suite cleanup relies on the
+ // standard "@tests.bookhoard.internal / test-named rows" passes of the
+ // next run's setupTestServer.
+}
+
+// createItemViaDB makes a throwaway library + media item directly through
+// the test DB handle — no HTTP, no t.Cleanup entanglement.
+func (s *OwnershipAndAdminTestSuite) createItemViaDB() string {
+ ctx := s.setup.QueueCtx
+ db := s.setup.DB
+
+ libraryTypeRow, err := db.GetLibraryTypeByName(ctx, "ebooks")
+ require.NoError(s.T(), err, "should find ebooks library type")
+
+ admin, err := db.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
+ require.NoError(s.T(), err, "dev admin user should exist")
+
+ library, err := db.CreateLibrary(ctx, database.CreateLibraryParams{
+ Name: fmt.Sprintf("Ownership Suite Library %d", time.Now().UnixNano()),
+ Description: pgtype.Text{String: "annotation ownership suite", Valid: true},
+ LibraryTypeID: libraryTypeRow.ID,
+ CreatedByAdminID: admin.ID,
+ })
+ require.NoError(s.T(), err)
+
+ item, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
+ LibraryID: library.ID,
+ Title: "Ownership Suite Item",
+ Author: pgtype.Text{String: "Suite Author", Valid: true},
+ FilePath: fmt.Sprintf("/tmp/ownership-suite-%d.epub", time.Now().UnixNano()),
+ MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
+ })
+ require.NoError(s.T(), err)
+
+ return uuid.UUID(item.ID.Bytes).String()
+}
+
+func (s *OwnershipAndAdminTestSuite) TearDownSuite() {
+ s.setup.Close()
+}
+
+func (s *OwnershipAndAdminTestSuite) getItem(itemID string) map[string]interface{} {
+ req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/media-items/"+itemID, nil)
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ require.Equal(s.T(), http.StatusOK, resp.StatusCode)
+ var body map[string]interface{}
+ require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&body))
+ return body
+}
+
+func (s *OwnershipAndAdminTestSuite) createHighlight(token string) map[string]interface{} {
+ payload, _ := json.Marshal(map[string]interface{}{
+ "selection_text": "owned annotation text",
+ "start_position": "p1",
+ "end_position": "p1",
+ "color": "#ffff00",
+ })
+ req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/media-items/"+s.itemID+"/highlights", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ require.Equal(s.T(), http.StatusCreated, resp.StatusCode, "highlight create should succeed")
+ var body map[string]interface{}
+ require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&body))
+ return body
+}
+
+func (s *OwnershipAndAdminTestSuite) highlightExists(token, highlightID string) bool {
+ req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/media-items/"+s.itemID+"/highlights", nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ var body []map[string]interface{}
+ require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&body))
+ for _, h := range body {
+ if h["id"] == highlightID {
+ return true
+ }
+ }
+ return false
+}
+
+// TestHighlightDelete_CrossUserNoOp pins the ownership predicate: user B's
+// DELETE of user A's highlight by bare id must not remove A's row.
+func (s *OwnershipAndAdminTestSuite) TestHighlightDelete_CrossUserNoOp() {
+ regular := s.setup.RegularToken
+
+ created := s.createHighlight(regular)
+ highlightID, ok := created["id"].(string)
+ require.True(s.T(), ok, "create response carries the highlight id")
+
+ // The ADMIN (a different user) tries to delete it by id.
+ req, _ := http.NewRequest("DELETE", s.setup.Server.URL+"/api/media-items/"+s.itemID+"/highlights/"+highlightID, nil)
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+
+ assert.True(s.T(), s.highlightExists(regular, highlightID),
+ "another user's delete-by-id must not remove the owner's highlight")
+
+ // The owner deletes their own highlight — gone.
+ req, _ = http.NewRequest("DELETE", s.setup.Server.URL+"/api/media-items/"+s.itemID+"/highlights/"+highlightID, nil)
+ req.Header.Set("Authorization", "Bearer "+regular)
+ resp, err = (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+
+ assert.False(s.T(), s.highlightExists(regular, highlightID), "owner delete removes the row")
+}
+
+// TestAdminCreateUser pins the admin counterpart to registration: admins
+// create accounts even with self-service registration disabled, regular
+// users cannot, and duplicates conflict.
+func (s *OwnershipAndAdminTestSuite) TestAdminCreateUser() {
+ suffix := fmt.Sprintf("%d", s.setup.DBPool.Config().MaxConns) // stable-ish uniquifier
+ unique := fmt.Sprintf("admincreated%s", suffix)
+
+ // Regular user token → 403 (AdminMiddleware).
+ payload, _ := json.Marshal(map[string]interface{}{
+ "email": unique + "@tests.bookhoard.internal",
+ "username": unique,
+ "password": "Test@Pass123!",
+ })
+ req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/auth/users", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+s.setup.RegularToken)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+ require.Equal(s.T(), http.StatusForbidden, resp.StatusCode)
+
+ // Admin → 201, and the account is immediately usable (login works)
+ // even though self-service registration is off for new servers.
+ req, _ = http.NewRequest("POST", s.setup.Server.URL+"/api/auth/users", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ resp, err = (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ require.Equal(s.T(), http.StatusCreated, resp.StatusCode)
+ var created map[string]interface{}
+ require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&created))
+ assert.Equal(s.T(), unique, created["username"])
+ assert.Equal(s.T(), "user", created["role"])
+
+ // Duplicate username → 409.
+ req, _ = http.NewRequest("POST", s.setup.Server.URL+"/api/auth/users", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ resp, err = (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ assert.Equal(s.T(), http.StatusConflict, resp.StatusCode)
+
+ // The created user can log in.
+ loginPayload, _ := json.Marshal(map[string]interface{}{
+ "login": unique + "@tests.bookhoard.internal",
+ "password": "Test@Pass123!",
+ })
+ req, _ = http.NewRequest("POST", s.setup.Server.URL+"/api/auth/login", bytes.NewBuffer(loginPayload))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err = (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ defer resp.Body.Close()
+ assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
+}
+
+func TestOwnershipAndAdminTestSuite(t *testing.T) {
+ suite.Run(t, new(OwnershipAndAdminTestSuite))
+}
diff --git a/cmd/server/tests/progress_service_test.go b/cmd/server/tests/progress_service_test.go
index 239d99e5..9af7dd8b 100644
--- a/cmd/server/tests/progress_service_test.go
+++ b/cmd/server/tests/progress_service_test.go
@@ -667,14 +667,16 @@ func TestDeletedUserTokenRejected(t *testing.T) {
ctx := context.Background()
// Create a dedicated user, then log in as it to get a fresh token.
- createResp := doReq(t, "POST", setup.Server.URL+"/api/auth/register", map[string]interface{}{
+ // Admin-created (not self-registered) so the test stays deterministic
+ // regardless of the registration_enabled setting.
+ createResp := doReq(t, "POST", setup.Server.URL+"/api/auth/users", map[string]interface{}{
"email": "ghost-test@tests.bookhoard.internal",
"username": "ghosttest",
"password": "Ghost@Pass123!",
}, setup.Token)
defer createResp.Body.Close()
if createResp.StatusCode != http.StatusCreated {
- t.Skipf("registration unavailable (status %d)", createResp.StatusCode)
+ t.Skipf("admin user creation unavailable (status %d)", createResp.StatusCode)
}
loginResp := doReq(t, "POST", setup.Server.URL+"/api/auth/login", map[string]interface{}{
diff --git a/cmd/server/tests/series_integration_test.go b/cmd/server/tests/series_integration_test.go
index 15ef4086..3cf32bc1 100644
--- a/cmd/server/tests/series_integration_test.go
+++ b/cmd/server/tests/series_integration_test.go
@@ -32,11 +32,11 @@ func (s *SeriesIntegrationTestSuite) TearDownSuite() {
}
func (s *SeriesIntegrationTestSuite) TestGetSeries_RequiresLibraryID() {
- // Ground truth: library_id is OPTIONAL on /api/series by design —
- // GetDistinctSeries matches (narg IS NULL OR library_id = narg), so a
- // missing library_id aggregates series across all libraries. The
- // former "library_id required → 400" expectation asserted validation
- // the shipped query never had.
+ // Ground truth (2026-10-13 visibility hardening): library_id stays
+ // optional, but the unscoped aggregate now covers only the requester's
+ // VISIBLE libraries — hidden libraries contribute nothing. Series are
+ // grouped per library (same name in two libraries = two entries), so
+ // every row carries a library_id.
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series", nil)
req.Header.Set("Authorization", "Bearer "+s.token)
@@ -54,6 +54,13 @@ func (s *SeriesIntegrationTestSuite) TestGetSeries_RequiresLibraryID() {
require.NoError(s.T(), err)
assert.Contains(s.T(), body, "series", "response carries the series array")
assert.Contains(s.T(), body, "total")
+ if rows, ok := body["series"].([]interface{}); ok {
+ for _, r := range rows {
+ row, ok := r.(map[string]interface{})
+ require.True(s.T(), ok)
+ assert.NotEmpty(s.T(), row["library_id"], "series rows carry their library scope")
+ }
+ }
}
func (s *SeriesIntegrationTestSuite) TestGetSeries_InvalidLibraryID() {
@@ -165,14 +172,13 @@ func (s *SeriesIntegrationTestSuite) TestGetSeries_LimitClampedTo100() {
assert.Equal(s.T(), float64(100), limit, "limit should be clamped to 100")
}
-func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_NameOnlyContract() {
- // Ground truth: /api/series/books requires only `name` — the app
- // client sends name-only (BookhoardApi.kt) and the web SSR page too
- // (frontend.go). The query is intentionally library-agnostic (it
- // matches the series name across libraries; library_id, if sent, is
- // ignored — nothing validates it). The former "library_id required /
- // bad uuid → 400" expectations asserted validation that never
- // shipped.
+func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_LibraryIDRequired() {
+ // Ground truth (2026-10-13 visibility hardening): /api/series/books is
+ // per-library now — library_id is REQUIRED, validated, and must be
+ // visible to the requester. The former name-only contract let any
+ // authenticated account pull series books across ALL libraries
+ // (including hidden ones), and merged same-name series across
+ // libraries (manga adaptations / LN splits).
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?name=Test+Series", nil)
req.Header.Set("Authorization", "Bearer "+s.token)
@@ -183,14 +189,12 @@ func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_NameOnlyContract() {
_ = Body.Close()
}(resp.Body)
- assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
+ assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
- var body map[string]interface{}
+ var body map[string]string
err = json.NewDecoder(resp.Body).Decode(&body)
require.NoError(s.T(), err)
- assert.Equal(s.T(), "Test Series", body["name"])
- assert.Contains(s.T(), body, "books", "response carries the books array")
- assert.Contains(s.T(), body, "total")
+ assert.Equal(s.T(), "library_id required", body["error"])
}
func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresName() {
@@ -214,8 +218,7 @@ func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresName() {
}
func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_InvalidLibraryID() {
- // Same name-only contract: an unparseable library_id is ignored, not
- // validated (nothing in the shipped pipeline ever reads it here).
+ // library_id is now validated: an unparseable uuid is a 400.
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?library_id=bad-uuid&name=Test", nil)
req.Header.Set("Authorization", "Bearer "+s.token)
@@ -226,7 +229,7 @@ func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_InvalidLibraryID() {
_ = Body.Close()
}(resp.Body)
- assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
+ assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
}
func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_NonexistentSeries() {
@@ -313,6 +316,83 @@ func (s *SeriesIntegrationTestSuite) TestGetSeries_ResponseStructure() {
assert.True(s.T(), ok, "'series' should be an array")
}
+func (s *SeriesIntegrationTestSuite) setLibraryVisibility(token, libraryID string, visible bool) *http.Response {
+ payload, _ := json.Marshal(map[string]interface{}{
+ "library_id": libraryID,
+ "is_visible": visible,
+ })
+ req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/libraries/visibility", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := (&http.Client{}).Do(req)
+ require.NoError(s.T(), err)
+ return resp
+}
+
+// TestGetSeries_HiddenLibraryScopedOut pins the family-lens contract: a
+// library hidden from a user contributes NOTHING to that user's series
+// views, and naming it explicitly is a 403, not a silent pass — across
+// /api/series, /api/series/books, and /api/media-items.
+func (s *SeriesIntegrationTestSuite) TestGetSeries_HiddenLibraryScopedOut() {
+ regular := s.setup.RegularToken
+
+ // Baseline: the regular user can see the suite library.
+ resp := s.setLibraryVisibility(regular, s.libraryID, true)
+ defer resp.Body.Close()
+ require.Equal(s.T(), http.StatusOK, resp.StatusCode)
+
+ // Hide it from the regular user.
+ resp = s.setLibraryVisibility(regular, s.libraryID, false)
+ require.Equal(s.T(), http.StatusOK, resp.StatusCode)
+ defer func() {
+ r := s.setLibraryVisibility(regular, s.libraryID, true)
+ _ = r.Body.Close()
+ }()
+
+ client := &http.Client{}
+
+ // Explicit hidden library → 403.
+ for _, path := range []string{
+ "/api/series?library_id=" + s.libraryID,
+ "/api/series/books?library_id=" + s.libraryID + "&name=Test",
+ "/api/media-items?library_id=" + s.libraryID,
+ } {
+ req, _ := http.NewRequest("GET", s.setup.Server.URL+path, nil)
+ req.Header.Set("Authorization", "Bearer "+regular)
+ r, err := client.Do(req)
+ require.NoError(s.T(), err)
+ body, _ := io.ReadAll(r.Body)
+ r.Body.Close()
+ require.Equalf(s.T(), http.StatusForbidden, r.StatusCode, "%s must be 403 for a hidden library (body: %s)", path, body)
+ }
+
+ // Unscoped aggregate: the hidden library's series never appear.
+ req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series", nil)
+ req.Header.Set("Authorization", "Bearer "+regular)
+ r, err := client.Do(req)
+ require.NoError(s.T(), err)
+ defer r.Body.Close()
+ require.Equal(s.T(), http.StatusOK, r.StatusCode)
+ var body map[string]interface{}
+ require.NoError(s.T(), json.NewDecoder(r.Body).Decode(&body))
+ if rows, ok := body["series"].([]interface{}); ok {
+ for _, row := range rows {
+ m, ok := row.(map[string]interface{})
+ require.True(s.T(), ok)
+ assert.NotEqual(s.T(), s.libraryID, m["library_id"], "hidden library must not appear in the aggregate")
+ }
+ }
+
+ // The admin still sees the library (default-allow; visibility rows are
+ // per-user).
+ req, _ = http.NewRequest("GET", s.setup.Server.URL+"/api/series?library_id="+s.libraryID, nil)
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ r, err = client.Do(req)
+ require.NoError(s.T(), err)
+ defer r.Body.Close()
+ assert.Equal(s.T(), http.StatusOK, r.StatusCode)
+}
+
func (s *SeriesIntegrationTestSuite) TestRestoreSystemCollection_ContinueSeries() {
reqBody := map[string]interface{}{
"collection_name": "Continue Series",
diff --git a/database/schema/schema.sql b/database/schema/schema.sql
index 04484657..68aa157c 100644
--- a/database/schema/schema.sql
+++ b/database/schema/schema.sql
@@ -68,7 +68,7 @@ INSERT INTO system_settings (setting_key, setting_value, description, setting_ty
('password_require_lower', 'true', 'Require at least one lowercase letter (a-z)', 'bool', NULL, NULL, FALSE, 'security'),
('password_require_number', 'true', 'Require at least one number (0-9)', 'bool', NULL, NULL, FALSE, 'security'),
('password_require_special', 'true', 'Require at least one special character', 'bool', NULL, NULL, FALSE, 'security'),
-('registration_enabled', 'true', 'Allow users to create their own accounts (admins can always create accounts)', 'bool', NULL, NULL, FALSE, 'security'),
+('registration_enabled', 'false', 'Allow users to create their own accounts (admins can always create accounts)', 'bool', NULL, NULL, FALSE, 'security'),
-- security / auth (restart required)
('auth_rate_limit_per_min', '10', 'Global auth API rate limit (requests per minute)', 'int', '1', '10000', TRUE, 'security'),
('login_max_attempts', '5', 'Failed login attempts before lockout', 'int', '1', '100', TRUE, 'security'),
diff --git a/internal/database/db.go b/internal/database/db.go
index 486aa36f..371b20c8 100644
--- a/internal/database/db.go
+++ b/internal/database/db.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.31.1
+// sqlc v1.31.0
package database
diff --git a/internal/database/models.go b/internal/database/models.go
index 8ce89d31..7f1b87eb 100644
--- a/internal/database/models.go
+++ b/internal/database/models.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.31.1
+// sqlc v1.31.0
package database
diff --git a/internal/database/querier.go b/internal/database/querier.go
index 8357c90f..a84280c0 100644
--- a/internal/database/querier.go
+++ b/internal/database/querier.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.31.1
+// sqlc v1.31.0
package database
@@ -131,12 +131,12 @@ type Querier interface {
DeleteKoboEntitlement(ctx context.Context, arg DeleteKoboEntitlementParams) error
DeleteLibrary(ctx context.Context, id pgtype.UUID) error
DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFolderParams) (LibraryFolders, error)
- DeleteMediaBookmark(ctx context.Context, id pgtype.UUID) error
- DeleteMediaHighlight(ctx context.Context, id pgtype.UUID) error
+ DeleteMediaBookmark(ctx context.Context, arg DeleteMediaBookmarkParams) error
+ DeleteMediaHighlight(ctx context.Context, arg DeleteMediaHighlightParams) error
DeleteMediaItem(ctx context.Context, id pgtype.UUID) error
// Delete media item format
DeleteMediaItemFormat(ctx context.Context, id pgtype.UUID) error
- DeleteMediaNote(ctx context.Context, id pgtype.UUID) error
+ DeleteMediaNote(ctx context.Context, arg DeleteMediaNoteParams) error
DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error
DeleteProcessingIssue(ctx context.Context, id pgtype.UUID) (ProcessingIssues, error)
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
@@ -205,8 +205,11 @@ type Querier interface {
// Get device shelf mappings
GetDeviceShelfMappings(ctx context.Context, deviceID pgtype.UUID) ([]GetDeviceShelfMappingsRow, error)
GetDictionaryEntry(ctx context.Context, word string) (DictionaryCache, error)
+ // Series are grouped PER LIBRARY: the same series name in two libraries
+ // (novels vs manga adaptations, LN splits) stays two entries. Visible
+ // filter enforces per-user library visibility at the source.
GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error)
- GetDistinctSeriesCount(ctx context.Context, libraryID pgtype.UUID) (int32, error)
+ GetDistinctSeriesCount(ctx context.Context, arg GetDistinctSeriesCountParams) (int32, error)
GetFailedSyncQueueItems(ctx context.Context, limit int32) ([]SyncQueue, error)
GetFirstAdmin(ctx context.Context) (pgtype.UUID, error)
GetFirstAdminExclude(ctx context.Context, id pgtype.UUID) (GetFirstAdminExcludeRow, error)
@@ -306,7 +309,10 @@ type Querier interface {
GetRefreshToken(ctx context.Context, token pgtype.UUID) (GetRefreshTokenRow, error)
GetSavedFilterByID(ctx context.Context, arg GetSavedFilterByIDParams) (SavedFilters, error)
GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams) ([]SavedFilters, error)
- GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]MediaItems, error)
+ // Scoped to ONE library: same-name series in different libraries are
+ // different stories (manga adaptations, LN splits). Callers must pass a
+ // library_id and verify the requester can see that library.
+ GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) ([]MediaItems, error)
GetSeriesCovers(ctx context.Context, arg GetSeriesCoversParams) ([]GetSeriesCoversRow, error)
GetStuckSyncQueueItems(ctx context.Context) ([]SyncQueue, error)
GetSyncConflict(ctx context.Context, id pgtype.UUID) (SyncConflicts, error)
@@ -460,11 +466,11 @@ type Querier interface {
SetSystemConfig(ctx context.Context, arg SetSystemConfigParams) (SystemConfig, error)
SyncLibraryTypeExtensions(ctx context.Context, arg SyncLibraryTypeExtensionsParams) error
TombstoneMediaBookmarkByDedupKey(ctx context.Context, arg TombstoneMediaBookmarkByDedupKeyParams) error
- TombstoneMediaBookmarkByID(ctx context.Context, id pgtype.UUID) error
+ TombstoneMediaBookmarkByID(ctx context.Context, arg TombstoneMediaBookmarkByIDParams) error
TombstoneMediaHighlightByDedupKey(ctx context.Context, arg TombstoneMediaHighlightByDedupKeyParams) error
- TombstoneMediaHighlightByID(ctx context.Context, id pgtype.UUID) error
+ TombstoneMediaHighlightByID(ctx context.Context, arg TombstoneMediaHighlightByIDParams) error
TombstoneMediaNoteByDedupKey(ctx context.Context, arg TombstoneMediaNoteByDedupKeyParams) error
- TombstoneMediaNoteByID(ctx context.Context, id pgtype.UUID) error
+ TombstoneMediaNoteByID(ctx context.Context, arg TombstoneMediaNoteByIDParams) error
TombstoneSystemCollection(ctx context.Context, id pgtype.UUID) error
// Update collection
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error)
diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go
index 74d5295d..262c67ac 100644
--- a/internal/database/queries.sql.go
+++ b/internal/database/queries.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.31.1
+// sqlc v1.31.0
// source: queries.sql
package database
@@ -2159,20 +2159,30 @@ func (q *Queries) DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFold
const DeleteMediaBookmark = `-- name: DeleteMediaBookmark :exec
DELETE FROM media_bookmarks
-WHERE id = $1
+WHERE id = $1 AND user_id = $2
`
-func (q *Queries) DeleteMediaBookmark(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, DeleteMediaBookmark, id)
+type DeleteMediaBookmarkParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) DeleteMediaBookmark(ctx context.Context, arg DeleteMediaBookmarkParams) error {
+ _, err := q.db.Exec(ctx, DeleteMediaBookmark, arg.ID, arg.UserID)
return err
}
const DeleteMediaHighlight = `-- name: DeleteMediaHighlight :exec
-DELETE FROM media_highlights WHERE id = $1
+DELETE FROM media_highlights WHERE id = $1 AND user_id = $2
`
-func (q *Queries) DeleteMediaHighlight(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, DeleteMediaHighlight, id)
+type DeleteMediaHighlightParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) DeleteMediaHighlight(ctx context.Context, arg DeleteMediaHighlightParams) error {
+ _, err := q.db.Exec(ctx, DeleteMediaHighlight, arg.ID, arg.UserID)
return err
}
@@ -2196,11 +2206,16 @@ func (q *Queries) DeleteMediaItemFormat(ctx context.Context, id pgtype.UUID) err
}
const DeleteMediaNote = `-- name: DeleteMediaNote :exec
-DELETE FROM media_notes WHERE id = $1
+DELETE FROM media_notes WHERE id = $1 AND user_id = $2
`
-func (q *Queries) DeleteMediaNote(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, DeleteMediaNote, id)
+type DeleteMediaNoteParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) DeleteMediaNote(ctx context.Context, arg DeleteMediaNoteParams) error {
+ _, err := q.db.Exec(ctx, DeleteMediaNote, arg.ID, arg.UserID)
return err
}
@@ -3889,31 +3904,45 @@ func (q *Queries) GetDictionaryEntry(ctx context.Context, word string) (Dictiona
}
const GetDistinctSeries = `-- name: GetDistinctSeries :many
-SELECT series, COUNT(*) as book_count,
+SELECT library_id, series, COUNT(*) as book_count,
MAX(series_count) as total_in_series,
MAX(created_at) as last_entry_at
FROM media_items
-WHERE ($1::uuid IS NULL OR library_id = $1::uuid) AND series IS NOT NULL AND series != ''
-GROUP BY series
+WHERE ($1::uuid IS NULL OR library_id = $1::uuid)
+ AND ($2::uuid[] IS NULL OR library_id = ANY($2::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series IS NOT NULL AND series != ''
+GROUP BY library_id, series
ORDER BY MAX(created_at) DESC
-LIMIT $3 OFFSET $2
+LIMIT $4 OFFSET $3
`
type GetDistinctSeriesParams struct {
- LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
- Offset pgtype.Int4 `db:"offset" json:"offset"`
- Limit pgtype.Int4 `db:"limit" json:"limit"`
+ LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
+ VisibleLibraryIds []pgtype.UUID `db:"visible_library_ids" json:"visible_library_ids"`
+ Offset pgtype.Int4 `db:"offset" json:"offset"`
+ Limit pgtype.Int4 `db:"limit" json:"limit"`
}
type GetDistinctSeriesRow struct {
+ LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Series pgtype.Text `db:"series" json:"series"`
BookCount int64 `db:"book_count" json:"book_count"`
TotalInSeries interface{} `db:"total_in_series" json:"total_in_series"`
LastEntryAt interface{} `db:"last_entry_at" json:"last_entry_at"`
}
+// Series are grouped PER LIBRARY: the same series name in two libraries
+// (novels vs manga adaptations, LN splits) stays two entries. Visible
+// filter enforces per-user library visibility at the source.
func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error) {
- rows, err := q.db.Query(ctx, GetDistinctSeries, arg.LibraryID, arg.Offset, arg.Limit)
+ rows, err := q.db.Query(ctx, GetDistinctSeries,
+ arg.LibraryID,
+ arg.VisibleLibraryIds,
+ arg.Offset,
+ arg.Limit,
+ )
if err != nil {
return nil, err
}
@@ -3922,6 +3951,7 @@ func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesPa
for rows.Next() {
var i GetDistinctSeriesRow
if err := rows.Scan(
+ &i.LibraryID,
&i.Series,
&i.BookCount,
&i.TotalInSeries,
@@ -3938,13 +3968,22 @@ func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesPa
}
const GetDistinctSeriesCount = `-- name: GetDistinctSeriesCount :one
-SELECT COUNT(DISTINCT series)::int
+SELECT COUNT(DISTINCT (library_id, series))::int
FROM media_items
-WHERE ($1::uuid IS NULL OR library_id = $1::uuid) AND series IS NOT NULL AND series != ''
+WHERE ($1::uuid IS NULL OR library_id = $1::uuid)
+ AND ($2::uuid[] IS NULL OR library_id = ANY($2::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series IS NOT NULL AND series != ''
`
-func (q *Queries) GetDistinctSeriesCount(ctx context.Context, libraryID pgtype.UUID) (int32, error) {
- row := q.db.QueryRow(ctx, GetDistinctSeriesCount, libraryID)
+type GetDistinctSeriesCountParams struct {
+ LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
+ VisibleLibraryIds []pgtype.UUID `db:"visible_library_ids" json:"visible_library_ids"`
+}
+
+func (q *Queries) GetDistinctSeriesCount(ctx context.Context, arg GetDistinctSeriesCountParams) (int32, error) {
+ row := q.db.QueryRow(ctx, GetDistinctSeriesCount, arg.LibraryID, arg.VisibleLibraryIds)
var column_1 int32
err := row.Scan(&column_1)
return column_1, err
@@ -7102,11 +7141,22 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams
const GetSeriesBooks = `-- name: GetSeriesBooks :many
SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items
WHERE series = $1
+ AND library_id = $2
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
ORDER BY series_number ASC NULLS LAST
`
-func (q *Queries) GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]MediaItems, error) {
- rows, err := q.db.Query(ctx, GetSeriesBooks, series)
+type GetSeriesBooksParams struct {
+ Series pgtype.Text `db:"series" json:"series"`
+ LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
+}
+
+// Scoped to ONE library: same-name series in different libraries are
+// different stories (manga adaptations, LN splits). Callers must pass a
+// library_id and verify the requester can see that library.
+func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) ([]MediaItems, error) {
+ rows, err := q.db.Query(ctx, GetSeriesBooks, arg.Series, arg.LibraryID)
if err != nil {
return nil, err
}
@@ -7193,15 +7243,20 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]Med
const GetSeriesCovers = `-- name: GetSeriesCovers :many
SELECT cover_image_path, library_id
FROM media_items
-WHERE ($1::uuid IS NULL OR library_id = $1::uuid) AND series = $2 AND cover_image_path IS NOT NULL AND cover_image_path != ''
+WHERE ($1::uuid IS NULL OR library_id = $1::uuid)
+ AND ($2::uuid[] IS NULL OR library_id = ANY($2::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series = $3 AND cover_image_path IS NOT NULL AND cover_image_path != ''
ORDER BY series_number ASC NULLS LAST
-LIMIT $3
+LIMIT $4
`
type GetSeriesCoversParams struct {
- LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
- Series pgtype.Text `db:"series" json:"series"`
- Limit pgtype.Int4 `db:"limit" json:"limit"`
+ LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
+ VisibleLibraryIds []pgtype.UUID `db:"visible_library_ids" json:"visible_library_ids"`
+ Series pgtype.Text `db:"series" json:"series"`
+ Limit pgtype.Int4 `db:"limit" json:"limit"`
}
type GetSeriesCoversRow struct {
@@ -7210,7 +7265,12 @@ type GetSeriesCoversRow struct {
}
func (q *Queries) GetSeriesCovers(ctx context.Context, arg GetSeriesCoversParams) ([]GetSeriesCoversRow, error) {
- rows, err := q.db.Query(ctx, GetSeriesCovers, arg.LibraryID, arg.Series, arg.Limit)
+ rows, err := q.db.Query(ctx, GetSeriesCovers,
+ arg.LibraryID,
+ arg.VisibleLibraryIds,
+ arg.Series,
+ arg.Limit,
+ )
if err != nil {
return nil, err
}
@@ -9201,12 +9261,14 @@ JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
WHERE mi.archived_at IS NULL
AND mi.missing_scan_count = 0
+ AND ($3::uuid[] IS NULL OR mi.library_id = ANY($3::uuid[]))
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
`
type ListMediaItemsParams struct {
- Limit int32 `db:"limit" json:"limit"`
- Offset int32 `db:"offset" json:"offset"`
+ Limit int32 `db:"limit" json:"limit"`
+ Offset int32 `db:"offset" json:"offset"`
+ VisibleLibraryIds []pgtype.UUID `db:"visible_library_ids" json:"visible_library_ids"`
}
type ListMediaItemsRow struct {
@@ -9279,7 +9341,7 @@ type ListMediaItemsRow struct {
}
func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) {
- rows, err := q.db.Query(ctx, ListMediaItems, arg.Limit, arg.Offset)
+ rows, err := q.db.Query(ctx, ListMediaItems, arg.Limit, arg.Offset, arg.VisibleLibraryIds)
if err != nil {
return nil, err
}
@@ -12167,11 +12229,16 @@ UPDATE media_bookmarks SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $2
`
-func (q *Queries) TombstoneMediaBookmarkByID(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, TombstoneMediaBookmarkByID, id)
+type TombstoneMediaBookmarkByIDParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) TombstoneMediaBookmarkByID(ctx context.Context, arg TombstoneMediaBookmarkByIDParams) error {
+ _, err := q.db.Exec(ctx, TombstoneMediaBookmarkByID, arg.ID, arg.UserID)
return err
}
@@ -12199,11 +12266,16 @@ UPDATE media_highlights SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $2
`
-func (q *Queries) TombstoneMediaHighlightByID(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, TombstoneMediaHighlightByID, id)
+type TombstoneMediaHighlightByIDParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) TombstoneMediaHighlightByID(ctx context.Context, arg TombstoneMediaHighlightByIDParams) error {
+ _, err := q.db.Exec(ctx, TombstoneMediaHighlightByID, arg.ID, arg.UserID)
return err
}
@@ -12231,11 +12303,16 @@ UPDATE media_notes SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $2
`
-func (q *Queries) TombstoneMediaNoteByID(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, TombstoneMediaNoteByID, id)
+type TombstoneMediaNoteByIDParams struct {
+ ID pgtype.UUID `db:"id" json:"id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
+}
+
+func (q *Queries) TombstoneMediaNoteByID(ctx context.Context, arg TombstoneMediaNoteByIDParams) error {
+ _, err := q.db.Exec(ctx, TombstoneMediaNoteByID, arg.ID, arg.UserID)
return err
}
@@ -12832,7 +12909,7 @@ UPDATE media_bookmarks SET
created_at = created_at,
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $14
RETURNING id, media_item_id, user_id, page_number, chapter_number, cfi_position, title, position, notes, created_at, dedup_key, last_modified_at, last_modified_source, device_sync_data, percentage_location, epubcfi_location, chapter_reference, deleted, deleted_at, origin_source
`
@@ -12850,6 +12927,7 @@ type UpdateMediaBookmarkForSyncParams struct {
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) UpdateMediaBookmarkForSync(ctx context.Context, arg UpdateMediaBookmarkForSyncParams) (MediaBookmarks, error) {
@@ -12867,6 +12945,7 @@ func (q *Queries) UpdateMediaBookmarkForSync(ctx context.Context, arg UpdateMedi
arg.LastModifiedAt,
arg.LastModifiedSource,
arg.DeviceSyncData,
+ arg.UserID,
)
var i MediaBookmarks
err := row.Scan(
@@ -12902,7 +12981,7 @@ UPDATE media_highlights SET
color = $5,
note_id = $6,
updated_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $7
RETURNING id, media_item_id, user_id, selection_text, start_position, end_position, color, note_id, created_at, updated_at, percentage_start, percentage_end, character_start, character_end, epubcfi_start, epubcfi_end, chapter_reference, paragraph_start, paragraph_end, panel_number, device_sync_data, dedup_key, last_modified_at, last_modified_source, note_text, deleted, deleted_at
`
@@ -12913,6 +12992,7 @@ type UpdateMediaHighlightParams struct {
EndPosition pgtype.Text `db:"end_position" json:"end_position"`
Color pgtype.Text `db:"color" json:"color"`
NoteID pgtype.UUID `db:"note_id" json:"note_id"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) UpdateMediaHighlight(ctx context.Context, arg UpdateMediaHighlightParams) (MediaHighlights, error) {
@@ -12923,6 +13003,7 @@ func (q *Queries) UpdateMediaHighlight(ctx context.Context, arg UpdateMediaHighl
arg.EndPosition,
arg.Color,
arg.NoteID,
+ arg.UserID,
)
var i MediaHighlights
err := row.Scan(
@@ -12975,7 +13056,7 @@ UPDATE media_highlights SET
updated_at = NOW(),
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $15
RETURNING id, media_item_id, user_id, selection_text, start_position, end_position, color, note_id, created_at, updated_at, percentage_start, percentage_end, character_start, character_end, epubcfi_start, epubcfi_end, chapter_reference, paragraph_start, paragraph_end, panel_number, device_sync_data, dedup_key, last_modified_at, last_modified_source, note_text, deleted, deleted_at
`
@@ -12994,6 +13075,7 @@ type UpdateMediaHighlightForSyncParams struct {
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) UpdateMediaHighlightForSync(ctx context.Context, arg UpdateMediaHighlightForSyncParams) (MediaHighlights, error) {
@@ -13012,6 +13094,7 @@ func (q *Queries) UpdateMediaHighlightForSync(ctx context.Context, arg UpdateMed
arg.LastModifiedAt,
arg.LastModifiedSource,
arg.DeviceSyncData,
+ arg.UserID,
)
var i MediaHighlights
err := row.Scan(
@@ -13617,7 +13700,7 @@ UPDATE media_notes SET
content = $2,
position = $3,
updated_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $4
RETURNING id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at
`
@@ -13625,10 +13708,16 @@ type UpdateMediaNoteParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Content string `db:"content" json:"content"`
Position pgtype.Text `db:"position" json:"position"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) UpdateMediaNote(ctx context.Context, arg UpdateMediaNoteParams) (MediaNotes, error) {
- row := q.db.QueryRow(ctx, UpdateMediaNote, arg.ID, arg.Content, arg.Position)
+ row := q.db.QueryRow(ctx, UpdateMediaNote,
+ arg.ID,
+ arg.Content,
+ arg.Position,
+ arg.UserID,
+ )
var i MediaNotes
err := row.Scan(
&i.ID,
@@ -13670,7 +13759,7 @@ UPDATE media_notes SET
updated_at = NOW(),
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $13
RETURNING id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at
`
@@ -13687,6 +13776,7 @@ type UpdateMediaNoteForSyncParams struct {
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
+ UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) UpdateMediaNoteForSync(ctx context.Context, arg UpdateMediaNoteForSyncParams) (MediaNotes, error) {
@@ -13703,6 +13793,7 @@ func (q *Queries) UpdateMediaNoteForSync(ctx context.Context, arg UpdateMediaNot
arg.LastModifiedAt,
arg.LastModifiedSource,
arg.DeviceSyncData,
+ arg.UserID,
)
var i MediaNotes
err := row.Scan(
diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql
index 76d0801d..ff0190a0 100644
--- a/internal/database/queries/queries.sql
+++ b/internal/database/queries/queries.sql
@@ -177,6 +177,7 @@ JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
WHERE mi.archived_at IS NULL
AND mi.missing_scan_count = 0
+ AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR mi.library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2;
-- name: ListMediaItemsByLibrary :many
@@ -870,11 +871,11 @@ UPDATE media_notes SET
content = $2,
position = $3,
updated_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $4
RETURNING *;
-- name: DeleteMediaNote :exec
-DELETE FROM media_notes WHERE id = $1;
+DELETE FROM media_notes WHERE id = $1 AND user_id = $2;
-- Media Highlights queries
-- name: CreateMediaHighlight :one
@@ -896,11 +897,11 @@ UPDATE media_highlights SET
color = $5,
note_id = $6,
updated_at = NOW()
-WHERE id = $1
+WHERE id = $1 AND user_id = $7
RETURNING *;
-- name: DeleteMediaHighlight :exec
-DELETE FROM media_highlights WHERE id = $1;
+DELETE FROM media_highlights WHERE id = $1 AND user_id = $2;
-- ============================================
-- ANNOTATION SYNC QUERIES (highlights)
@@ -943,7 +944,7 @@ UPDATE media_highlights SET
updated_at = NOW(),
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $15
RETURNING *;
-- name: TombstoneMediaHighlightByDedupKey :exec
@@ -958,7 +959,7 @@ UPDATE media_highlights SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1;
+WHERE id = $1 AND user_id = $2;
-- name: PurgeExpiredHighlightTombstones :exec
DELETE FROM media_highlights WHERE deleted = TRUE AND deleted_at < $1;
@@ -1000,7 +1001,7 @@ UPDATE media_notes SET
updated_at = NOW(),
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $13
RETURNING *;
-- name: TombstoneMediaNoteByDedupKey :exec
@@ -1015,7 +1016,7 @@ UPDATE media_notes SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1;
+WHERE id = $1 AND user_id = $2;
-- name: PurgeExpiredNoteTombstones :exec
DELETE FROM media_notes WHERE deleted = TRUE AND deleted_at < $1;
@@ -1058,7 +1059,7 @@ UPDATE media_bookmarks SET
created_at = created_at,
deleted = FALSE,
deleted_at = NULL
-WHERE id = $1
+WHERE id = $1 AND user_id = $14
RETURNING *;
-- name: TombstoneMediaBookmarkByDedupKey :exec
@@ -1073,7 +1074,7 @@ UPDATE media_bookmarks SET
deleted = TRUE,
deleted_at = NOW(),
last_modified_at = NOW()
-WHERE id = $1;
+WHERE id = $1 AND user_id = $2;
-- name: PurgeExpiredBookmarkTombstones :exec
DELETE FROM media_bookmarks WHERE deleted = TRUE AND deleted_at < $1;
@@ -2635,30 +2636,51 @@ WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('libra
ORDER BY mi.created_at DESC;
-- name: GetDistinctSeries :many
-SELECT series, COUNT(*) as book_count,
+-- Series are grouped PER LIBRARY: the same series name in two libraries
+-- (novels vs manga adaptations, LN splits) stays two entries. Visible
+-- filter enforces per-user library visibility at the source.
+SELECT library_id, series, COUNT(*) as book_count,
MAX(series_count) as total_in_series,
MAX(created_at) as last_entry_at
FROM media_items
-WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid) AND series IS NOT NULL AND series != ''
-GROUP BY series
+WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid)
+ AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series IS NOT NULL AND series != ''
+GROUP BY library_id, series
ORDER BY MAX(created_at) DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetDistinctSeriesCount :one
-SELECT COUNT(DISTINCT series)::int
+SELECT COUNT(DISTINCT (library_id, series))::int
FROM media_items
-WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid) AND series IS NOT NULL AND series != '';
+WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid)
+ AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series IS NOT NULL AND series != '';
-- name: GetSeriesCovers :many
SELECT cover_image_path, library_id
FROM media_items
-WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid) AND series = sqlc.narg('series') AND cover_image_path IS NOT NULL AND cover_image_path != ''
+WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_id')::uuid)
+ AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
+ AND series = sqlc.narg('series') AND cover_image_path IS NOT NULL AND cover_image_path != ''
ORDER BY series_number ASC NULLS LAST
LIMIT sqlc.narg('limit');
-- name: GetSeriesBooks :many
+-- Scoped to ONE library: same-name series in different libraries are
+-- different stories (manga adaptations, LN splits). Callers must pass a
+-- library_id and verify the requester can see that library.
SELECT * FROM media_items
WHERE series = sqlc.narg('series')
+ AND library_id = sqlc.narg('library_id')
+ AND archived_at IS NULL
+ AND missing_scan_count = 0
ORDER BY series_number ASC NULLS LAST;
-- name: GetContinueSeriesItems :many
@@ -2799,7 +2821,7 @@ RETURNING *;
-- name: DeleteMediaBookmark :exec
DELETE FROM media_bookmarks
-WHERE id = $1;
+WHERE id = $1 AND user_id = $2;
-- name: UpdateMediaBookmark :one
UPDATE media_bookmarks
diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go
index 1142294d..6ce7a1eb 100644
--- a/internal/handlers/auth.go
+++ b/internal/handlers/auth.go
@@ -10,6 +10,7 @@ import (
"context"
"errors"
"fmt"
+ "html/template"
"net/http"
"os"
"path/filepath"
@@ -686,6 +687,79 @@ func (h *AuthHandler) ListUsers(c *echo.Context) error {
return c.JSON(http.StatusOK, users)
}
+// AdminCreateUser handles POST /api/auth/users (AdminMiddleware-gated).
+// The admin's counterpart to public registration: creating an account for
+// someone else must work even when self-service registration is disabled
+// (the family/self-hosted default). The requester's admin role comes from
+// the route group, and the new account NEVER auto-logins — no tokens are
+// issued here, unlike Register.
+func (h *AuthHandler) AdminCreateUser(c *echo.Context) error {
+ var req RegisterRequest
+ if err := c.Bind(&req); err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ }
+ if err := c.Validate(&req); err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
+ }
+
+ req.Username = strings.TrimSpace(req.Username)
+ if req.Username == "" {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "username cannot be empty or whitespace"})
+ }
+
+ role := strings.ToLower(req.Role)
+ if role == "" {
+ role = "user"
+ }
+ if role != "user" && role != "admin" {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid role. must be 'user' or 'admin'"})
+ }
+
+ if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
+ return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"})
+ }
+ if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil {
+ return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
+ }
+
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
+ }
+
+ user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{
+ Email: req.Email,
+ Username: req.Username,
+ PasswordHash: string(hashedPassword),
+ FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
+ LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
+ Theme: pgtype.Text{String: "tokyo-night", Valid: true},
+ Role: role,
+ })
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ }
+
+ if err := h.CreateDefaultCollectionsForUser(c.Request().Context(), user.ID); err != nil {
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create default collections"})
+ }
+
+ // The admin UI posts this via HTMX: refresh the page so the new user
+ // appears in the table, and confirm inline.
+ if c.Request().Header.Get("HX-Request") == "true" {
+ c.Response().Header().Set("HX-Refresh", "true")
+ return c.HTML(http.StatusCreated, `
User "`+template.HTMLEscapeString(user.Username)+`" created.
`)
+ }
+
+ return c.JSON(http.StatusCreated, map[string]interface{}{
+ "id": uuid.UUID(user.ID.Bytes).String(),
+ "email": user.Email,
+ "username": user.Username,
+ "role": user.Role,
+ "created_at": user.CreatedAt,
+ })
+}
+
// normalizePath cleans and normalizes folder paths for consistent storage and comparison
func normalizePath(path string) string {
fmt.Printf("normalizePath input: '%s'\n", path)
diff --git a/internal/handlers/conflicts.go b/internal/handlers/conflicts.go
index 6b6023d4..74de54b7 100644
--- a/internal/handlers/conflicts.go
+++ b/internal/handlers/conflicts.go
@@ -406,6 +406,7 @@ func (h *ConflictHandler) applyHighlightResolution(ctx context.Context, mediaIte
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
DeviceSyncData: existing.DeviceSyncData,
+ UserID: userID,
}
if v, ok := data["selection_text"].(string); ok {
@@ -451,6 +452,7 @@ func (h *ConflictHandler) applyBookmarkResolution(ctx context.Context, mediaItem
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
DeviceSyncData: existing.DeviceSyncData,
+ UserID: userID,
}
if v, ok := data["title"].(string); ok {
@@ -486,6 +488,7 @@ func (h *ConflictHandler) applyNoteResolution(ctx context.Context, mediaItemID p
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
DeviceSyncData: existing.DeviceSyncData,
+ UserID: userID,
}
if v, ok := data["content"].(string); ok {
diff --git a/internal/handlers/media.go b/internal/handlers/media.go
index 616345b9..06bf44c2 100644
--- a/internal/handlers/media.go
+++ b/internal/handlers/media.go
@@ -626,6 +626,29 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error {
})
}
+// visibleLibrarySet loads the requester's visible libraries — membership
+// map for param checks, id slice for SQL ANY() filters.
+func (mh *MediaHandler) visibleLibrarySet(c *echo.Context) (map[string]bool, []pgtype.UUID, error) {
+ user, ok := c.Get("user").(database.Users)
+ if !ok {
+ return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
+ }
+ libraries, err := mh.libraryService.GetUserVisibleLibraries(c.Request().Context(), user.ID)
+ if err != nil {
+ return nil, nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to check library access")
+ }
+ set := make(map[string]bool, len(libraries))
+ ids := make([]pgtype.UUID, 0, len(libraries))
+ for _, lib := range libraries {
+ if !lib.ID.Valid {
+ continue
+ }
+ set[uuid.UUID(lib.ID.Bytes).String()] = true
+ ids = append(ids, lib.ID)
+ }
+ return set, ids, nil
+}
+
// ListMediaItems handles GET /api/media-items
func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
libraryID := c.QueryParam("library_id")
@@ -678,6 +701,14 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
}
+ visibleSet, _, err := mh.visibleLibrarySet(c)
+ if err != nil {
+ return err
+ }
+ if !visibleSet[libraryID] {
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"})
+ }
+
items, err := mh.db.ListMediaItemsSorted(c.Request().Context(), database.ListMediaItemsSortedParams{
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Sort: pgtype.Text{String: sort, Valid: true},
@@ -691,9 +722,14 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{"data": items})
}
+ _, visibleIDs, err := mh.visibleLibrarySet(c)
+ if err != nil {
+ return err
+ }
items, err := mh.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
- Limit: int32(limit),
- Offset: int32(offset),
+ Limit: int32(limit),
+ Offset: int32(offset),
+ VisibleLibraryIds: visibleIDs,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -764,6 +800,15 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
+ // Visibility gate: hidden libraries don't leak row detail by id.
+ visibleSet, _, err := mh.visibleLibrarySet(c)
+ if err != nil {
+ return err
+ }
+ if !visibleSet[uuid.UUID(item.LibraryID.Bytes).String()] {
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"})
+ }
+
return c.JSON(http.StatusOK, map[string]interface{}{
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
@@ -1666,6 +1711,12 @@ func (mh *MediaHandler) UpdateMediaNote(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
+ userID := c.Get("user_id").(string)
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
+ }
+
var req UpdateMediaNoteRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
@@ -1678,6 +1729,7 @@ func (mh *MediaHandler) UpdateMediaNote(c *echo.Context) error {
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
+ UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1694,10 +1746,19 @@ func (mh *MediaHandler) DeleteMediaNote(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
+ userID := c.Get("user_id").(string)
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
+ }
+
if mh.annotationSvc != nil {
- err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
+ err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}, pgtype.UUID{Bytes: userUUID, Valid: true})
} else {
- err = mh.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
+ err = mh.db.DeleteMediaNote(c.Request().Context(), database.DeleteMediaNoteParams{
+ ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
+ UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
+ })
}
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1899,6 +1960,12 @@ func (mh *MediaHandler) UpdateMediaHighlight(c *echo.Context) error {
return c.JSON(http.StatusOK, result.Highlight)
}
+ userID := c.Get("user_id").(string)
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
+ }
+
highlight, err := mh.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
SelectionText: req.SelectionText,
@@ -1906,6 +1973,7 @@ func (mh *MediaHandler) UpdateMediaHighlight(c *echo.Context) error {
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
+ UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1924,14 +1992,27 @@ func (mh *MediaHandler) DeleteMediaHighlight(c *echo.Context) error {
pgHighlightID := pgtype.UUID{Bytes: highlightUUID, Valid: true}
+ userID := c.Get("user_id").(string)
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
+ }
+ pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
+
if mh.annotationSvc != nil {
- if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgHighlightID, "web"); err != nil {
+ if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgUserID, pgHighlightID, "web"); err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
+ }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
- err = mh.db.DeleteMediaHighlight(c.Request().Context(), pgHighlightID)
+ err = mh.db.DeleteMediaHighlight(c.Request().Context(), database.DeleteMediaHighlightParams{
+ ID: pgHighlightID,
+ UserID: pgUserID,
+ })
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
@@ -2076,14 +2157,27 @@ func (mh *MediaHandler) DeleteMediaBookmark(c *echo.Context) error {
pgBookmarkID := pgtype.UUID{Bytes: bookmarkUUID, Valid: true}
+ userID := c.Get("user_id").(string)
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
+ }
+ pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
+
if mh.annotationSvc != nil {
- if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgBookmarkID, "web"); err != nil {
+ if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgUserID, pgBookmarkID, "web"); err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return c.JSON(http.StatusNotFound, map[string]string{"error": "bookmark not found"})
+ }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
- if err := mh.db.DeleteMediaBookmark(c.Request().Context(), pgBookmarkID); err != nil {
+ if err := mh.db.DeleteMediaBookmark(c.Request().Context(), database.DeleteMediaBookmarkParams{
+ ID: pgBookmarkID,
+ UserID: pgUserID,
+ }); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
diff --git a/internal/handlers/reader.go b/internal/handlers/reader.go
index e59effff..7af83dbc 100644
--- a/internal/handlers/reader.go
+++ b/internal/handlers/reader.go
@@ -213,6 +213,27 @@ func (h *ReaderHandler) GetPage(c *echo.Context) error {
}
// GetChapters returns chapter metadata
+// ensureItemVisible enforces the library-visibility gate for reader
+// content and layout endpoints (same rule GetPage applies): a hidden
+// library leaks nothing by item id.
+func (h *ReaderHandler) ensureItemVisible(c *echo.Context, mediaItem *database.MediaItems) error {
+ user := c.Get("user")
+ if user == nil {
+ return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"})
+ }
+ userData := user.(database.Users)
+ libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
+ }
+ for _, lib := range libraries {
+ if lib.ID == mediaItem.LibraryID {
+ return nil
+ }
+ }
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
+}
+
func (h *ReaderHandler) GetChapters(c *echo.Context) error {
mediaItemID := c.Param("mediaItemId")
parsedUUID, err := uuid.Parse(mediaItemID)
@@ -220,6 +241,14 @@ func (h *ReaderHandler) GetChapters(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
}
+ mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
+ if err != nil {
+ return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
+ }
+ if err := h.ensureItemVisible(c, &mediaItem); err != nil {
+ return err
+ }
+
// Use reader service to detect chapters
chapters, err := h.readerService.DetectChapters(c.Request().Context(), parsedUUID)
if err != nil {
@@ -246,6 +275,14 @@ func (h *ReaderHandler) GetPanels(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid page number"})
}
+ mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
+ if err != nil {
+ return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
+ }
+ if err := h.ensureItemVisible(c, &mediaItem); err != nil {
+ return err
+ }
+
// Get detection method from query
method := c.QueryParam("method")
if method == "" {
@@ -398,6 +435,9 @@ func (h *ReaderHandler) GetPDFOutline(c *echo.Context) error {
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
}
+ if err := h.ensureItemVisible(c, &mediaItem); err != nil {
+ return err
+ }
// Only PDFs have outlines
if mediaItem.FormatGroup != "pdf" {
@@ -438,6 +478,9 @@ func (h *ReaderHandler) GetPDFThumbnail(c *echo.Context) error {
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
}
+ if err := h.ensureItemVisible(c, &mediaItem); err != nil {
+ return err
+ }
// Get thumbnail size from query
width := c.QueryParam("width")
diff --git a/internal/handlers/series.go b/internal/handlers/series.go
index 2cd72a54..afade132 100644
--- a/internal/handlers/series.go
+++ b/internal/handlers/series.go
@@ -50,13 +50,25 @@ func (h *SeriesHandler) GetSeries(c *echo.Context) error {
}
}
- seriesList, total, err := h.seriesService.GetSeriesPage(c.Request().Context(), libUUID, limit, offset)
+ // Visibility scoping: an explicit library_id must be visible to the
+ // requester (403 otherwise); the unscoped aggregate covers only the
+ // requester's visible libraries.
+ visibleSet, visibleIDs, err := h.visibleLibrarySet(c)
+ if err != nil {
+ return err
+ }
+ if libraryID != "" && !visibleSet[libraryID] {
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"})
+ }
+
+ seriesList, total, err := h.seriesService.GetSeriesPage(c.Request().Context(), libUUID, visibleIDs, limit, offset)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series"})
}
type SeriesResponse struct {
Name string `json:"name"`
+ LibraryID string `json:"library_id"`
BookCount int64 `json:"book_count"`
TotalInSeries int `json:"total_in_series"`
CoverPaths []string `json:"cover_paths"`
@@ -67,6 +79,7 @@ func (h *SeriesHandler) GetSeries(c *echo.Context) error {
for _, s := range seriesList {
response = append(response, SeriesResponse{
Name: s.Name,
+ LibraryID: s.LibraryID,
BookCount: s.BookCount,
TotalInSeries: s.TotalInSeries,
CoverPaths: s.CoverPaths,
@@ -82,13 +95,58 @@ func (h *SeriesHandler) GetSeries(c *echo.Context) error {
})
}
+// visibleLibrarySet loads the requester's visible libraries. Every series
+// path funnels through this: hidden libraries contribute nothing, and an
+// explicit library_id outside the set is a 403, not a silent pass.
+func (h *SeriesHandler) visibleLibrarySet(c *echo.Context) (map[string]bool, []pgtype.UUID, error) {
+ user, ok := c.Get("user").(database.Users)
+ if !ok {
+ return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
+ }
+ libraries, err := h.seriesService.VisibleLibraries(c.Request().Context(), user.ID)
+ if err != nil {
+ return nil, nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to check library access")
+ }
+ set := make(map[string]bool, len(libraries))
+ ids := make([]pgtype.UUID, 0, len(libraries))
+ for _, lib := range libraries {
+ if !lib.ID.Valid {
+ continue
+ }
+ libUUID, err := uuid.FromBytes(lib.ID.Bytes[0:16])
+ if err != nil {
+ continue
+ }
+ set[libUUID.String()] = true
+ ids = append(ids, lib.ID)
+ }
+ return set, ids, nil
+}
+
func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error {
seriesName := c.QueryParam("name")
if seriesName == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "name required"})
}
- books, err := h.seriesService.GetSeriesBooks(c.Request().Context(), seriesName)
+ libraryID := c.QueryParam("library_id")
+ if libraryID == "" {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
+ }
+ libUUID, err := uuid.Parse(libraryID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
+ }
+
+ visibleSet, _, err := h.visibleLibrarySet(c)
+ if err != nil {
+ return err
+ }
+ if !visibleSet[libraryID] {
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"})
+ }
+
+ books, err := h.seriesService.GetSeriesBooks(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true}, seriesName)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series books"})
}
@@ -105,13 +163,14 @@ func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error {
}
return c.JSON(http.StatusOK, map[string]interface{}{
- "name": seriesName,
- "books": bookCards,
- "total": len(bookCards),
+ "name": seriesName,
+ "library_id": libraryID,
+ "books": bookCards,
+ "total": len(bookCards),
})
}
-func GetSeriesCardsData(ctx context.Context, db *database.Queries, libraryID pgtype.UUID, limit, offset int) ([]services.SeriesInfo, int, error) {
+func GetSeriesCardsData(ctx context.Context, db *database.Queries, libraryID pgtype.UUID, visibleLibraryIDs []pgtype.UUID, limit, offset int) ([]services.SeriesInfo, int, error) {
svc := services.NewSeriesService(db)
- return svc.GetSeriesPage(ctx, libraryID, limit, offset)
+ return svc.GetSeriesPage(ctx, libraryID, visibleLibraryIDs, limit, offset)
}
diff --git a/internal/router/auth.go b/internal/router/auth.go
index a02a8706..47608b13 100644
--- a/internal/router/auth.go
+++ b/internal/router/auth.go
@@ -40,6 +40,7 @@ func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) {
// Admin-only routes (same handlers with URL param)
admin := protected.Group("/auth", handlers.AdminMiddleware)
admin.GET("/users", cfg.AuthHandler.ListUsers)
+ admin.POST("/users", cfg.AuthHandler.AdminCreateUser)
admin.PUT("/users/:id/max-devices", cfg.AuthHandler.UpdateUserMaxDevices)
admin.PUT("/profile/:id", cfg.AuthHandler.UpdateProfile)
admin.PUT("/password/:id", cfg.AuthHandler.UpdatePassword)
diff --git a/internal/router/frontend.go b/internal/router/frontend.go
index 709aae9c..2a87be06 100644
--- a/internal/router/frontend.go
+++ b/internal/router/frontend.go
@@ -151,27 +151,38 @@ func registerFrontendRoutes(cfg *Config) {
totalPages := 1
if errorMsg == "" {
- seriesList, total, err := handlers.GetSeriesCardsData(c.Request().Context(), cfg.Queries, libRes.LibUUID, perSeriesPage, offset)
- if err != nil {
- log.Printf("GetSeriesCardsData failed: %v", err)
+ // Visible-library scoping: cards are grouped per library, and a
+ // specific library selection only renders if it's in the set.
+ visibleSet, visibleIDs, visErr := visibleLibraryIDsFor(c, cfg)
+ if visErr != nil {
+ log.Printf("visibleLibraryIDsFor failed: %v", visErr)
errorMsg = "Error loading series"
+ } else if libRes.LibUUID.Valid && !visibleSet[libRes.LibraryID] {
+ errorMsg = "Library not accessible"
} else {
- totalPages = (total + perSeriesPage - 1) / perSeriesPage
- if totalPages < 1 {
- totalPages = 1
- }
- seriesCards = make([]templates.SeriesCardData, 0, len(seriesList))
- for _, s := range seriesList {
- covers := s.CoverPaths
- if covers == nil {
- covers = []string{}
+ seriesList, total, err := handlers.GetSeriesCardsData(c.Request().Context(), cfg.Queries, libRes.LibUUID, visibleIDs, perSeriesPage, offset)
+ if err != nil {
+ log.Printf("GetSeriesCardsData failed: %v", err)
+ errorMsg = "Error loading series"
+ } else {
+ totalPages = (total + perSeriesPage - 1) / perSeriesPage
+ if totalPages < 1 {
+ totalPages = 1
+ }
+ seriesCards = make([]templates.SeriesCardData, 0, len(seriesList))
+ for _, s := range seriesList {
+ covers := s.CoverPaths
+ if covers == nil {
+ covers = []string{}
+ }
+ seriesCards = append(seriesCards, templates.SeriesCardData{
+ Name: s.Name,
+ LibraryID: s.LibraryID,
+ BookCount: s.BookCount,
+ TotalInSeries: s.TotalInSeries,
+ CoverPaths: covers,
+ })
}
- seriesCards = append(seriesCards, templates.SeriesCardData{
- Name: s.Name,
- BookCount: s.BookCount,
- TotalInSeries: s.TotalInSeries,
- CoverPaths: covers,
- })
}
}
}
@@ -202,10 +213,28 @@ func registerFrontendRoutes(cfg *Config) {
return renderErrorPage(c, "Series name required", "bad_request")
}
+ // Series are per-library now: the detail page must say WHICH library's
+ // series it is showing, and the requester must be able to see it.
+ libraryIDParam := c.QueryParam("library_id")
+ if libraryIDParam == "" {
+ return renderErrorPage(c, "Library required", "bad_request")
+ }
+ libUUIDParam, err := uuid.Parse(libraryIDParam)
+ if err != nil {
+ return renderErrorPage(c, "Invalid library", "bad_request")
+ }
+ visibleSet, _, visErr := visibleLibraryIDsFor(c, cfg)
+ if visErr != nil {
+ return renderErrorPage(c, "Error checking access", "access_error")
+ }
+ if !visibleSet[libraryIDParam] {
+ return renderErrorPage(c, "Access denied", "access_denied")
+ }
+
var bookInfoList []handlers.BookInfo
svc := services.NewSeriesService(cfg.Queries)
- books, err := svc.GetSeriesBooks(c.Request().Context(), seriesName)
+ books, err := svc.GetSeriesBooks(c.Request().Context(), pgtype.UUID{Bytes: libUUIDParam, Valid: true}, seriesName)
if err != nil {
log.Printf("GetSeriesBooks failed: %v", err)
errorMsg = "Error loading series books"
diff --git a/internal/router/helpers.go b/internal/router/helpers.go
index c3b610e7..7eabf64d 100644
--- a/internal/router/helpers.go
+++ b/internal/router/helpers.go
@@ -3,6 +3,7 @@ package router
import (
"context"
"log"
+ "net/http"
"net/url"
"os"
"time"
@@ -118,6 +119,15 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu
log.Printf("GetUserVisibleLibraries failed: %v", err)
libraries = []database.GetUserVisibleLibrariesRow{}
}
+ visibleSet := make(map[string]bool, len(libraries))
+ for _, lib := range libraries {
+ if !lib.ID.Valid {
+ continue
+ }
+ if id, err := uuid.FromBytes(lib.ID.Bytes[0:16]); err == nil {
+ visibleSet[id.String()] = true
+ }
+ }
counts, countErr := cfg.Queries.GetVisibleLibraryMediaCounts(c.Request().Context(), uuidToPGType(userU))
if countErr != nil {
@@ -189,8 +199,47 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu
return res
}
+ // A requested library outside the requester's visible set is not
+ // honored — fall back to the first visible library instead of leaking
+ // hidden-library contents through a hand-typed query param or cookie.
+ if !visibleSet[libraryID] {
+ res.LibraryID = res.FirstID
+ if res.LibraryID != "" {
+ parsed, _ := uuid.Parse(res.LibraryID)
+ res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
+ }
+ return res
+ }
+
res.LibraryID = libraryID
parsed, _ := uuid.Parse(libraryID)
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
return res
}
+
+// visibleLibraryIDsFor returns the requester's visible-library id set (for
+// membership checks) plus the ids themselves (for SQL ANY() filters).
+func visibleLibraryIDsFor(c *echo.Context, cfg *Config) (map[string]bool, []pgtype.UUID, error) {
+ userUUIDStr, ok := c.Get("user_id").(string)
+ if !ok {
+ return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
+ }
+ userU, err := uuid.Parse(userUUIDStr)
+ if err != nil {
+ return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
+ }
+ libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userU))
+ if err != nil {
+ return nil, nil, err
+ }
+ set := make(map[string]bool, len(libraries))
+ ids := make([]pgtype.UUID, 0, len(libraries))
+ for _, lib := range libraries {
+ if !lib.ID.Valid {
+ continue
+ }
+ set[lib.ID.String()] = true
+ ids = append(ids, lib.ID)
+ }
+ return set, ids, nil
+}
diff --git a/internal/services/series_service.go b/internal/services/series_service.go
index e3457e25..7ef61989 100644
--- a/internal/services/series_service.go
+++ b/internal/services/series_service.go
@@ -5,11 +5,13 @@ import (
"bookhoard/internal/utils"
"context"
+ "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type SeriesInfo struct {
Name string
+ LibraryID string
BookCount int64
TotalInSeries int
CoverPaths []string
@@ -24,16 +26,25 @@ func NewSeriesService(db *database.Queries) *SeriesService {
return &SeriesService{db: db}
}
-func (s *SeriesService) GetSeriesPage(ctx context.Context, libraryID pgtype.UUID, limit, offset int) ([]SeriesInfo, int, error) {
- totalCount, err := s.db.GetDistinctSeriesCount(ctx, libraryID)
+// GetSeriesPage lists series grouped PER LIBRARY (same name in two
+// libraries = two entries) filtered to the requester's visible libraries.
+// Pass libraryID for a single-library page, or leave it invalid and pass
+// visibleLibraryIDs for an across-my-libraries page. Both filters compose:
+// the handler validates any explicit library_id against the visible set.
+func (s *SeriesService) GetSeriesPage(ctx context.Context, libraryID pgtype.UUID, visibleLibraryIDs []pgtype.UUID, limit, offset int) ([]SeriesInfo, int, error) {
+ totalCount, err := s.db.GetDistinctSeriesCount(ctx, database.GetDistinctSeriesCountParams{
+ LibraryID: libraryID,
+ VisibleLibraryIds: visibleLibraryIDs,
+ })
if err != nil {
return nil, 0, err
}
rows, err := s.db.GetDistinctSeries(ctx, database.GetDistinctSeriesParams{
- LibraryID: libraryID,
- Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
- Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
+ LibraryID: libraryID,
+ VisibleLibraryIds: visibleLibraryIDs,
+ Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
+ Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
})
if err != nil {
return nil, 0, err
@@ -42,7 +53,7 @@ func (s *SeriesService) GetSeriesPage(ctx context.Context, libraryID pgtype.UUID
var series []SeriesInfo
for _, row := range rows {
seriesName := row.Series.String
- coverPaths, _ := s.GetSeriesCovers(ctx, libraryID, seriesName, 7)
+ coverPaths, _ := s.GetSeriesCovers(ctx, row.LibraryID, seriesName, 7)
totalInSeries := 0
if row.TotalInSeries != nil {
@@ -71,6 +82,7 @@ func (s *SeriesService) GetSeriesPage(ctx context.Context, libraryID pgtype.UUID
series = append(series, SeriesInfo{
Name: seriesName,
+ LibraryID: uuidFromPG(row.LibraryID),
BookCount: row.BookCount,
TotalInSeries: totalInSeries,
CoverPaths: coverPaths,
@@ -106,8 +118,28 @@ func (s *SeriesService) GetSeriesCovers(ctx context.Context, libraryID pgtype.UU
return paths, nil
}
-func (s *SeriesService) GetSeriesBooks(ctx context.Context, seriesName string) ([]database.MediaItems, error) {
- return s.db.GetSeriesBooks(ctx, pgtype.Text{String: seriesName, Valid: true})
+func (s *SeriesService) GetSeriesBooks(ctx context.Context, libraryID pgtype.UUID, seriesName string) ([]database.MediaItems, error) {
+ return s.db.GetSeriesBooks(ctx, database.GetSeriesBooksParams{
+ Series: pgtype.Text{String: seriesName, Valid: true},
+ LibraryID: libraryID,
+ })
+}
+
+// VisibleLibraries exposes the per-user visibility set used by the series
+// handlers to scope listings and validate explicit library_id params.
+func (s *SeriesService) VisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) {
+ return s.db.GetUserVisibleLibraries(ctx, userID)
+}
+
+func uuidFromPG(id pgtype.UUID) string {
+ if !id.Valid {
+ return ""
+ }
+ u, err := uuid.FromBytes(id.Bytes[0:16])
+ if err != nil {
+ return ""
+ }
+ return u.String()
}
func continueSeriesRowToMediaItems(row database.GetContinueSeriesItemsRow) database.MediaItems {
diff --git a/internal/sync/annotations.go b/internal/sync/annotations.go
index caa789e3..20b0ea12 100644
--- a/internal/sync/annotations.go
+++ b/internal/sync/annotations.go
@@ -258,6 +258,7 @@ func (s *AnnotationService) applyLWW(
LastModifiedAt: pgtype.Timestamptz{Time: modifiedAt, Valid: true},
LastModifiedSource: pgtype.Text{String: req.Source, Valid: req.Source != ""},
DeviceSyncData: deviceData,
+ UserID: req.UserID,
})
if err != nil {
return nil, fmt.Errorf("update highlight: %w", err)
@@ -328,14 +329,22 @@ func (s *AnnotationService) TombstoneHighlight(
func (s *AnnotationService) TombstoneHighlightByID(
ctx context.Context,
- highlightID pgtype.UUID,
+ userID, highlightID pgtype.UUID,
source string,
) error {
h, err := s.db.GetMediaHighlight(ctx, highlightID)
if err != nil {
return fmt.Errorf("get highlight for tombstone: %w", err)
}
- err = s.db.TombstoneMediaHighlightByID(ctx, highlightID)
+ // Ownership predicate: the caller must own the row. A mismatched user
+ // gets a not-found-style error, never the row's existence.
+ if h.UserID != userID {
+ return fmt.Errorf("tombstone highlight by ID: %w", pgx.ErrNoRows)
+ }
+ err = s.db.TombstoneMediaHighlightByID(ctx, database.TombstoneMediaHighlightByIDParams{
+ ID: highlightID,
+ UserID: userID,
+ })
if err != nil {
return fmt.Errorf("tombstone highlight by ID: %w", err)
}
@@ -646,8 +655,11 @@ func (s *AnnotationService) compareIncomingNote(req SaveNoteRequest, existing da
return req.ModifiedAt.After(existingMod.Time), true
}
-func (s *AnnotationService) TombstoneNoteByID(ctx context.Context, id pgtype.UUID) error {
- return s.db.TombstoneMediaNoteByID(ctx, id)
+func (s *AnnotationService) TombstoneNoteByID(ctx context.Context, id, userID pgtype.UUID) error {
+ return s.db.TombstoneMediaNoteByID(ctx, database.TombstoneMediaNoteByIDParams{
+ ID: id,
+ UserID: userID,
+ })
}
type SaveBookmarkRequest struct {
@@ -788,6 +800,7 @@ func (s *AnnotationService) applyBookmarkLWW(ctx context.Context, req SaveBookma
LastModifiedAt: pgtype.Timestamptz{Time: modifiedAt, Valid: true},
LastModifiedSource: pgtype.Text{String: req.Source, Valid: req.Source != ""},
DeviceSyncData: deviceData,
+ UserID: req.UserID,
})
if err != nil {
return nil, fmt.Errorf("update bookmark: %w", err)
@@ -817,12 +830,19 @@ func (s *AnnotationService) compareIncomingBookmark(req SaveBookmarkRequest, exi
return req.ModifiedAt.After(existingMod.Time), true
}
-func (s *AnnotationService) TombstoneBookmarkByID(ctx context.Context, bookmarkID pgtype.UUID, source string) error {
+func (s *AnnotationService) TombstoneBookmarkByID(ctx context.Context, userID, bookmarkID pgtype.UUID, source string) error {
bm, err := s.db.GetMediaBookmark(ctx, bookmarkID)
if err != nil {
return fmt.Errorf("get bookmark for tombstone: %w", err)
}
- err = s.db.TombstoneMediaBookmarkByID(ctx, bookmarkID)
+ // Ownership predicate: the caller must own the row.
+ if bm.UserID != userID {
+ return fmt.Errorf("tombstone bookmark by ID: %w", pgx.ErrNoRows)
+ }
+ err = s.db.TombstoneMediaBookmarkByID(ctx, database.TombstoneMediaBookmarkByIDParams{
+ ID: bookmarkID,
+ UserID: userID,
+ })
if err != nil {
return fmt.Errorf("tombstone bookmark by ID: %w", err)
}
diff --git a/templates/admin_archived_templ.go b/templates/admin_archived_templ.go
index fd5fdb75..b51d6bf0 100644
--- a/templates/admin_archived_templ.go
+++ b/templates/admin_archived_templ.go
@@ -57,7 +57,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(retentionDays)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 30, Col: 70}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 30, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -113,7 +113,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 65, Col: 82}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 65, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -131,7 +131,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 67, Col: 66}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 67, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -149,7 +149,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 69, Col: 103}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 69, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -162,7 +162,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.LibraryName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 71, Col: 86}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 71, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -175,7 +175,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 72, Col: 83}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 72, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -193,7 +193,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 77, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 77, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -206,7 +206,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.MatchID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 78, Col: 46}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 78, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -219,7 +219,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("Merge this archived copy's reading data into the active copy \"" + item.MatchTitle + "\" (" + item.MatchLibraryName + ") and remove the archived entry")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 80, Col: 174}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 80, Col: 174}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -245,7 +245,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 88, Col: 37}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 88, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -266,7 +266,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 97, Col: 43}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 97, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_hash_conflicts_templ.go b/templates/admin_hash_conflicts_templ.go
index 061dd869..8ed25b05 100644
--- a/templates/admin_hash_conflicts_templ.go
+++ b/templates/admin_hash_conflicts_templ.go
@@ -59,7 +59,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("conflict-" + conflict.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 27, Col: 53}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 27, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -80,7 +80,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d copies", len(conflict.Items)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 33, Col: 87}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 33, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -93,7 +93,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.LibraryName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 36, Col: 62}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 36, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -106,7 +106,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.SHAShort)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 38, Col: 55}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 38, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflict.ID + "/resolve")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 46, Col: 70}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 46, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -132,7 +132,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflict.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 48, Col: 43}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 48, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -194,7 +194,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 71, Col: 82}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 71, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 73, Col: 82}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 73, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -230,7 +230,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 75, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 75, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -243,7 +243,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f MB", float64(item.FileSize)/(1024*1024)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 77, Col: 64}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 77, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -261,7 +261,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 79, Col: 85}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 79, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 81, Col: 43}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 81, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -297,7 +297,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflictID + "/resolve")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 87, Col: 68}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 87, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -310,7 +310,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf(`{"action": "keep", "keep_uuid": "%s"}`, item.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 88, Col: 75}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 88, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -323,7 +323,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflictID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 89, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 89, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -383,7 +383,7 @@ func AdminHashConflicts(user User, conflicts []HashConflictData) templ.Component
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 1, Col: 0}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_library_templ.go b/templates/admin_library_templ.go
index 7972cc5c..4263cf62 100644
--- a/templates/admin_library_templ.go
+++ b/templates/admin_library_templ.go
@@ -65,7 +65,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 42, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 42, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -211,7 +211,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 246, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 246, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -229,7 +229,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 248, Col: 95}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 248, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -251,7 +251,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 253, Col: 26}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 253, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -273,7 +273,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 258, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 258, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -291,7 +291,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + library.ID + "/scan")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 264, Col: 58}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 264, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -312,7 +312,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 274, Col: 32}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 274, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -333,7 +333,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-panel-" + library.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 283, Col: 44}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 283, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -408,7 +408,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(folder.FolderPath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 305, Col: 99}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 305, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -421,7 +421,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 308, Col: 62}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 308, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
@@ -434,7 +434,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 309, Col: 64}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 309, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -447,7 +447,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 310, Col: 49}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 310, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -478,7 +478,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 320, Col: 80}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -491,7 +491,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 320, Col: 124}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 124}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -504,7 +504,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 324, Col: 37}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 324, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -517,7 +517,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 331, Col: 32}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 331, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -530,7 +530,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 332, Col: 52}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 332, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -587,7 +587,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/visibility")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 361, Col: 63}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 361, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -600,7 +600,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"user_id": "` + u.ID + `"}`)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 362, Col: 47}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 362, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
@@ -613,7 +613,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 364, Col: 49}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 364, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
@@ -626,7 +626,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 367, Col: 76}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 367, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -639,7 +639,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 368, Col: 75}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 368, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -667,7 +667,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var26 templ.SafeURL
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs("/admin/libraries/" + libraryID + "/issues")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 377, Col: 57}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 377, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@@ -694,7 +694,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 382, Col: 24}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 382, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@@ -721,7 +721,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 391, Col: 28}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 391, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
@@ -734,7 +734,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 392, Col: 33}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 392, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil {
@@ -747,7 +747,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 393, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 393, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil {
@@ -768,7 +768,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 401, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 401, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -781,7 +781,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 402, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 402, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
@@ -839,7 +839,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 417, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 417, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
@@ -857,7 +857,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 423, Col: 117}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 423, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
@@ -888,7 +888,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 436, Col: 118}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 436, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
if templ_7745c5c3_Err != nil {
@@ -909,7 +909,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 441, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 441, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -927,7 +927,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(targetInput)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 448, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 448, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
if templ_7745c5c3_Err != nil {
@@ -940,7 +940,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 449, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 449, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_processing_issues_templ.go b/templates/admin_processing_issues_templ.go
index 3ecfb4c1..9940bbc2 100644
--- a/templates/admin_processing_issues_templ.go
+++ b/templates/admin_processing_issues_templ.go
@@ -70,7 +70,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 36, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 36, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -97,7 +97,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 45, Col: 94}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 45, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -124,7 +124,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 54, Col: 91}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 54, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -166,7 +166,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("issue-" + issue.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 70, Col: 54}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 70, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -179,7 +179,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 73, Col: 98}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 73, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -192,7 +192,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 96}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 74, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -205,7 +205,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 76, Col: 140}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 76, Col: 140}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -218,7 +218,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 77, Col: 144}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 77, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -231,7 +231,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 139}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 78, Col: 139}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -244,7 +244,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 149}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 79, Col: 149}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -262,7 +262,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 62}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 84, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -280,7 +280,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 86, Col: 63}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 86, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -298,7 +298,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 88, Col: 66}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 88, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -321,7 +321,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + libraryID + "/issues/" + issue.ID + "/" + issue.MediaItemID + "/resolve")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 95, Col: 113}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 95, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -334,7 +334,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("#issue-" + issue.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 96, Col: 43}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 96, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_settings_templ.go b/templates/admin_settings_templ.go
index 525f9d1d..9a1d8a50 100644
--- a/templates/admin_settings_templ.go
+++ b/templates/admin_settings_templ.go
@@ -70,7 +70,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 34, Col: 28}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 34, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 48, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 48, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -373,7 +373,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 109, Col: 145}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 109, Col: 145}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -386,7 +386,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 110, Col: 144}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 110, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -399,7 +399,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 111, Col: 152}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 111, Col: 152}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -479,7 +479,7 @@ func ScanSettingsSection(scanSettings ScanSettingsData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 158, Col: 69}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 158, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -571,7 +571,7 @@ func TunableSettingsSection(groups []SettingGroup, restartRequired bool) templ.C
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(g.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 198, Col: 112}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 198, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -629,7 +629,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 213, Col: 94}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 213, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -647,7 +647,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 215, Col: 74}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 215, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -665,7 +665,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 217, Col: 74}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 217, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -683,7 +683,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#status-" + e.Key)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 223, Col: 33}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 223, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -696,7 +696,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Key)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 227, Col: 48}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 227, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -739,7 +739,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 237, Col: 20}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 237, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -757,7 +757,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Min)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 239, Col: 17}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 239, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -776,7 +776,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Max)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 242, Col: 17}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 242, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -799,7 +799,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 250, Col: 20}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 250, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -825,7 +825,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("status-" + e.Key)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 259, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 259, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -878,7 +878,7 @@ func AboutSection() templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 273, Col: 134}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 273, Col: 134}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_templ.go b/templates/admin_templ.go
index 3d54462c..2df319a8 100644
--- a/templates/admin_templ.go
+++ b/templates/admin_templ.go
@@ -60,7 +60,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.LibraryCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 32, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 32, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -81,7 +81,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.MediaCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 39, Col: 90}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 39, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -102,7 +102,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.UserCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 46, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 46, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -123,7 +123,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.DeviceCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 53, Col: 91}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 53, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
diff --git a/templates/admin_users.templ b/templates/admin_users.templ
index 3cfa182f..ee68438e 100644
--- a/templates/admin_users.templ
+++ b/templates/admin_users.templ
@@ -25,6 +25,47 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
Manage user accounts and permissions
+
+
+
+ @Icon("user", "h-4 w-4")
+
Create User
+
+
+
+
diff --git a/templates/admin_users_templ.go b/templates/admin_users_templ.go
index af59c469..9513d07b 100644
--- a/templates/admin_users_templ.go
+++ b/templates/admin_users_templ.go
@@ -45,25 +45,33 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "User Management
Manage user accounts and permissions
| Username | Email | Role | Created | Actions |
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "User Management
Manage user accounts and permissions
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = Icon("user", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Create User
| Username | Email | Role | Created | Actions |
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, user := range users {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"transition-colors hover:bg-surface-hover\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -71,143 +79,143 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 50, Col: 97}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 91, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user.ID == currentUser.ID {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " You")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " You")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " | ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 59, Col: 80}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 100, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " | ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " | ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user.Role == "admin" && adminCount == 1 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Admin ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-xs mt-1\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " | ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " | ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 92, Col: 126}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 133, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " | | ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Edit ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user.Role == "admin" && adminCount == 1 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -267,17 +275,17 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Delete")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " | |
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "