Files
pinchflat/lib/pinchflat_web/router.ex
T
hermes-agent 75f75d8d19
Nix Flake Check / Nix Flake Check (push) Successful in 1m55s
Build and Test / Build and Test (push) Successful in 8m25s
Make API auth mandatory, manage tokens via web UI
BREAKING CHANGE: API authentication is now always required. The
PINCHFLAT_API_TOKEN env var is no longer used. Instead, tokens are
stored in the database and managed via Settings → API Access.

Changes:
- Add api_token column to settings table (migration)
- ApiAuthPlug reads from DB; returns 401 if no token configured
- Add API Access section to Settings page with generate/regenerate/revoke
- Add POST /settings/generate_api_token and /settings/revoke_api_token
- Remove api_token from config.exs and runtime.exs
- Update all API controller tests to set auth token in setup
- Update auth plug tests for mandatory authentication
- 1008 tests pass, zero warnings
2026-07-04 11:51:40 +00:00

130 lines
5.0 KiB
Elixir

defmodule PinchflatWeb.Router do
use PinchflatWeb, :router
import PinchflatWeb.Plugs
import Phoenix.LiveDashboard.Router
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
# the extension from the path
pipeline :browser do
plug :basic_auth
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {PinchflatWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :allow_iframe_embed
end
pipeline :api do
plug :accepts, ["json"]
end
pipeline :api_auth do
plug PinchflatWeb.ApiAuthPlug
end
scope "/", PinchflatWeb do
pipe_through [:maybe_basic_auth, :token_protected_route]
# has to match before /sources/:id
get "/sources/opml", Podcasts.PodcastController, :opml_feed
end
# Routes in here _may not be_ protected by basic auth. This is necessary for
# media streaming to work for RSS podcast feeds.
scope "/", PinchflatWeb do
pipe_through :maybe_basic_auth
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
end
scope "/", PinchflatWeb do
pipe_through :browser
get "/", Pages.PageController, :home
resources "/media_profiles", MediaProfiles.MediaProfileController
resources "/search", Searches.SearchController, only: [:show], singleton: true
resources "/settings", Settings.SettingController, only: [:show, :update], singleton: true
post "/settings/generate_api_token", Settings.SettingController, :generate_api_token
post "/settings/revoke_api_token", Settings.SettingController, :revoke_api_token
get "/app_info", Settings.SettingController, :app_info
get "/download_logs", Settings.SettingController, :download_logs
resources "/sources", Sources.SourceController do
post "/force_download_pending", Sources.SourceController, :force_download_pending
post "/force_redownload", Sources.SourceController, :force_redownload
post "/force_index", Sources.SourceController, :force_index
post "/force_metadata_refresh", Sources.SourceController, :force_metadata_refresh
post "/sync_files_on_disk", Sources.SourceController, :sync_files_on_disk
resources "/media", MediaItems.MediaItemController, only: [:show, :edit, :update, :delete] do
post "/force_download", MediaItems.MediaItemController, :force_download
end
end
end
# No auth or CSRF protection for the health check endpoint
scope "/", PinchflatWeb do
pipe_through :api
get "/healthcheck", HealthController, :check, log: false
end
# JSON API for MCP integration and other programmatic clients.
# Protected by Bearer token auth (PINCHFLAT_API_TOKEN env var).
scope "/api/v1", PinchflatWeb.Api.V1 do
pipe_through [:api, :api_auth]
get "/sources", ApiSourceController, :index
get "/sources/:id", ApiSourceController, :show
post "/sources", ApiSourceController, :create
put "/sources/:id", ApiSourceController, :update
patch "/sources/:id", ApiSourceController, :update
delete "/sources/:id", ApiSourceController, :delete
post "/sources/:source_id/force_download_pending", ApiSourceController, :force_download_pending
post "/sources/:source_id/force_redownload", ApiSourceController, :force_redownload
post "/sources/:source_id/force_index", ApiSourceController, :force_index
post "/sources/:source_id/force_metadata_refresh", ApiSourceController, :force_metadata_refresh
post "/sources/:source_id/sync_files_on_disk", ApiSourceController, :sync_files_on_disk
get "/media", ApiMediaItemController, :index
get "/media/search", ApiMediaItemController, :search
get "/media/:id", ApiMediaItemController, :show
put "/media/:id", ApiMediaItemController, :update
patch "/media/:id", ApiMediaItemController, :update
delete "/media/:id", ApiMediaItemController, :delete
post "/media/:media_item_id/force_download", ApiMediaItemController, :force_download
get "/media_profiles", ApiMediaProfileController, :index
get "/media_profiles/:id", ApiMediaProfileController, :show
post "/media_profiles", ApiMediaProfileController, :create
put "/media_profiles/:id", ApiMediaProfileController, :update
patch "/media_profiles/:id", ApiMediaProfileController, :update
delete "/media_profiles/:id", ApiMediaProfileController, :delete
get "/settings", ApiSettingController, :show
put "/settings", ApiSettingController, :update
patch "/settings", ApiSettingController, :update
get "/app_info", ApiSettingController, :app_info
get "/tasks", ApiTaskController, :index
get "/tasks/:id", ApiTaskController, :show
end
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard",
metrics: PinchflatWeb.Telemetry,
ecto_repos: [Pinchflat.Repo]
end
end