From 50e4d6a6e2d0a602c4e96ab37e0aa604e47961db Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Thu, 6 Aug 2026 03:29:24 +0700 Subject: [PATCH] feat: serve novel-bookmark.user.js and allow the novel sites' origins --- .env.example | 5 +++-- backend/api_test.go | 37 +++++++++++++++++++++++++++++++++++++ backend/main.go | 18 ++++++++++++------ docker-compose.yml | 5 ++++- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 51f093b..61b3f85 100644 --- a/.env.example +++ b/.env.example @@ -5,8 +5,9 @@ API_TOKEN=changeme-generate-a-long-random-token # Comma-separated origins allowed to call the API (CORS). Both Asura domains -# plus Demonic, Comix, and Kagane. Add/remove as the sites' hostnames change. -ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to +# plus Demonic, Comix, Kagane, and the two novel sites. Add/remove as the +# sites' hostnames change. +ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net # --- Prod override (Traefik) only --- # Subdomain Traefik routes to this service (required by the prod override). diff --git a/backend/api_test.go b/backend/api_test.go index 1efcfab..fafc1fa 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -470,3 +470,40 @@ func TestUserscriptServedWithWebUIDisabled(t *testing.T) { t.Fatalf("status = %d, want 200", rr.Code) } } + +// Both scripts are served from the same handler on the same token, outside the +// WEB_PASSWORD gate — a wrong token is a 404, never a 401. +func TestNovelUserscriptServed(t *testing.T) { + dir := t.TempDir() + novelPath := filepath.Join(dir, "novel-bookmark.user.js") + if err := os.WriteFile(novelPath, []byte("// novel\n"), 0o644); err != nil { + t.Fatalf("write script: %v", err) + } + + s, err := store.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("store.Open: %v", err) + } + t.Cleanup(func() { s.Close() }) + + cfg := testConfig() + cfg.NovelUserscriptPath = novelPath + srv := newRouter(s, cfg) + + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, + "/u/"+testToken+"/novel-bookmark.user.js", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", rr.Code) + } + if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") { + t.Fatalf("Content-Type = %q, want text/javascript", ct) + } + + rr = httptest.NewRecorder() + srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, + "/u/wrong-token/novel-bookmark.user.js", nil)) + if rr.Code != http.StatusNotFound { + t.Fatalf("wrong token status = %d, want 404", rr.Code) + } +} diff --git a/backend/main.go b/backend/main.go index faef2d9..275f5b8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -31,6 +31,10 @@ type Config struct { // UserscriptPath is the file served at /u/{token}/manga-bookmark.user.js. // Supplied by a bindmount so the script can be edited without a rebuild. UserscriptPath string + // NovelUserscriptPath is the file served at + // /u/{token}/novel-bookmark.user.js. Same bindmount, second script: the + // two libraries are separate installs. + NovelUserscriptPath string // LatestPoll configures the background latest-chapter fetcher. LatestPoll LatestPoll } @@ -138,12 +142,13 @@ func loadLatestPoll() LatestPoll { func loadConfig() Config { c := Config{ - Token: os.Getenv("API_TOKEN"), - DBPath: envOr("DB_PATH", "/data/bookmarks.db"), - Port: envOr("PORT", "8080"), - WebPassword: os.Getenv("WEB_PASSWORD"), - UserscriptPath: envOr("USERSCRIPT_PATH", "/userscript/manga-bookmark.user.js"), - LatestPoll: loadLatestPoll(), + Token: os.Getenv("API_TOKEN"), + DBPath: envOr("DB_PATH", "/data/bookmarks.db"), + Port: envOr("PORT", "8080"), + WebPassword: os.Getenv("WEB_PASSWORD"), + UserscriptPath: envOr("USERSCRIPT_PATH", "/userscript/manga-bookmark.user.js"), + NovelUserscriptPath: envOr("NOVEL_USERSCRIPT_PATH", "/userscript/novel-bookmark.user.js"), + LatestPoll: loadLatestPoll(), } for _, o := range strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") { if o = strings.TrimSpace(o); o != "" { @@ -164,6 +169,7 @@ func newRouter(s *store.Store, cfg Config) http.Handler { // outside the WEB_PASSWORD gate (the script must be installable either // way). The path segment carries the token instead. mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscript.Handler(cfg.Token, cfg.UserscriptPath)) + mux.HandleFunc("GET /u/{token}/novel-bookmark.user.js", userscript.Handler(cfg.Token, cfg.NovelUserscriptPath)) h := &api.Handler{Store: s} protected := http.NewServeMux() diff --git a/docker-compose.yml b/docker-compose.yml index 2eb4a39..bf3f0fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,13 +15,16 @@ services: environment: # API_TOKEN is required — compose refuses to start without it. API_TOKEN: ${API_TOKEN:?set API_TOKEN in .env} - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net} DB_PATH: /data/bookmarks.db PORT: "8080" # Gates the browser UI. Unset means the web routes are not served at all. WEB_PASSWORD: ${WEB_PASSWORD:-} # Path inside the container; matches the bindmount above. USERSCRIPT_PATH: ${USERSCRIPT_PATH:-/userscript/manga-bookmark.user.js} + # Second script from the same bindmount; the novel library is a separate + # Violentmonkey install. + NOVEL_USERSCRIPT_PATH: ${NOVEL_USERSCRIPT_PATH:-/userscript/novel-bookmark.user.js} # Latest-chapter poller. LATEST_CHAPTER_POLL_ENABLED=0 in .env is the kill # switch; it only takes effect because these are listed here. LATEST_CHAPTER_POLL_ENABLED: ${LATEST_CHAPTER_POLL_ENABLED:-1}