The "Advanced" settings section is gated to admins in the navigation, but
the controllers exposing family-wide data performed no server-side
authorization — any authenticated user could reach them by navigating
directly to the URL.
Add `before_action :require_admin!` to the controllers that expose
family-wide data:
- Settings::AiPromptsController (family AI assistant config)
- Settings::LlmUsagesController (family LLM usage/billing)
This mirrors the existing guards on Settings::ProvidersController and
Settings::HostingsController. Non-admins are redirected with the standard
"Only admins can perform this action" message (403 for turbo_stream/json).
API key and MCP token management are intentionally left open: both are
user-scoped self-management. Their actions are already scoped to
Current.user, and any signed-in user (not just admins) can create an API
key or authorize an MCP client, so gating them would leave members unable
to view or revoke their own credentials.
Adds controller tests covering admin-only access for the family-wide pages
(member and guest rejected) and member self-service access for the API key
and MCP pages.
* feat(mcp): add OAuth well-known discovery endpoints (RFC 8414 + RFC 9728)
Serves /.well-known/oauth-protected-resource (RFC 9728) and
/.well-known/oauth-authorization-server (RFC 8414) so MCP clients
can auto-discover the authorization server. Both endpoints are
unauthenticated and respect APP_URL for reverse-proxy deployments.
* feat(mcp): add dynamic client registration endpoint (RFC 7591)
POST /register creates a public Doorkeeper::Application on demand so
MCP clients (e.g. Claude.ai) can self-register without manual setup.
Validates redirect_uris (including blank entries), falls back to
"MCP Client" name, returns no client_secret (public client, PKCE only).
Rate-limited to 10 registrations/min/IP via Rack::Attack.
* feat(mcp): authenticate via Doorkeeper OAuth2, keep MCP_API_TOKEN as fallback
MCP endpoint now accepts OAuth2 Bearer tokens issued by Doorkeeper.
Falls back to the existing MCP_API_TOKEN env-var flow so self-hosted
deployments are not broken. Requires MCP_OAUTH_ENABLED or MCP_API_TOKEN
to be set — the endpoint returns 503 otherwise.
- OauthBase concern provides APP_URL-aware configured_base_url (trailing
slash stripped to prevent double-slash URLs)
- Bearer scheme parsed case-insensitively (RFC 7235)
- Only read_write scope accepted — read scope would allow mutating tools
(CreateGoal, ImportBankStatement), so read-only tokens are rejected
- Deactivated users rejected even with a valid Doorkeeper token
- WWW-Authenticate header on 401 points to RFC 9728 resource metadata
- SHA-256 digest used for constant-time env-var comparison
- Rack::Attack throttle added for POST /register
- Routes wired: /.well-known/*, /register, use_doorkeeper
* fix(mcp): disable Turbo on OAuth consent form for external redirect URIs
Turbo was intercepting the authorization form POST and XHR-fetching
the redirect_uri (e.g. https://claude.ai/api/mcp/auth_callback),
which CORS blocks. Extend the existing turbo_disabled guard to cover
any redirect_uri that doesn't originate from the app itself.
* feat(mcp): add Settings::McpController with connected clients view
- Settings > MCP page (under Advanced) shows the MCP server URL with
copy button and step-by-step instructions for connecting Claude.ai
- Lists active non-mobile OAuth tokens with app name and revoke action;
mobile device tokens are excluded to prevent accidental disconnection
- Removes the MCP_OAUTH_ENABLED env-var gate — OAuth auth is always
available since Doorkeeper handles consent; MCP_API_TOKEN remains
as a self-hosted fallback
* fix(mcp): remove client_credentials from grant_types_supported metadata
Only authorization_code is supported by the registration endpoint.
Advertising client_credentials was misleading — a client that reads
the metadata and attempts that flow would get an application with the
wrong grant type.