Add API V2 from DMPonline by gjacob24 · Pull Request #3587 · DMPRoadmap/roadmap
and others added 2 commits
November 19, 2025 15:06Fix crashes when `@client` is nil (e.g., on heartbeat endpoint where doorkeeper_token is not present). - `@client = doorkeeper_token&.application` simplifies `@client` assignment. - Guard log_access and add `@caller` for safe JSON output Align JSON response keys with V1 - server -> application - client -> caller Co-Authored-By: gjacob24 <97518971+gjacob24@users.noreply.github.com> Co-Authored-By: John Pinto <8876215+johnpinto1@users.noreply.github.com>
This test suite was adapted from the v2 API tests in CDLUC3/dmptool. The original request and view specs were copied and then refactored to align with our v2 API. Supporting factories and spec helper files were updated as needed to accommodate the new coverage.
Previously, `fetch_q_and_a` iterated over all plan questions and searched for matching answers in Ruby, performing theme filtering and Q&A formatting in memory. This caused unnecessary loops, potential N+1 lookups, and inefficient handling of multi-theme questions. Accessing questions directly via `Plan.questions` also triggered joins through sections, phases, and templates, along with ordering (due to default scope), which added query overhead. The refactored version: - Queries answers directly via a join to question themes, filtering at the DB level. - Accesses questions through the answers (`answer.question`) instead of `plan.questions`, avoiding heavy joins and unnecessary default-scoped sorting.
Previously, the /api/v2/plans endpoint triggered many N+1 queries when rendering plan associations, causing slow response times. This change adds .includes() for all associations that were previously triggering Bullet N+1 warnings. Benchmark comparison (local dev, 10 sequential requests): | Metric | Before | After | | ----------------- | ------- | ------ | | Mean request time | 1909 ms | 492 ms | | Requests/sec | 0.52 | 2.03 | | Max request time | 2628 ms | 641 ms | - This simple comparison shows that these changes make the `GET api/v2/plans` ~4x faster.
- Use warden.authenticate! (instead of manually storing session[:user_return_to] + explicitly redirecting to login page) - Simplify conditional logic by checking "native" path first - Replace hardcoded "/oauth/authorize/native path" string with native_oauth_authorization_path When warden.authenticate! is invoked, Warden both automatically stores the request URL in the session and reroutes the unauthenticated user to the login page. The existing session[:user_return_to] checks in ApplicationController continue to work. This reduces custom code and leverages Devise's built-in authentication handling for better maintainability. NOTE: For better alignment with Devise conventions and automatic session cleanup, we could refactor ApplicationController to use stored_location_for(resource) instead of session[:user_return_to].
- Use start_with?(oauth_authorization_path) instead of include?('/oauth/authorize') for stricter path matching
- Use stored_location_for(:user) instead of session[:user_return_to] for automatic cleanup and explicit Devise scope usage
- OAuth flow detection is tied to session[:user_return_to], so the :user scope is explicit and correct
- Add ? suffix to predicate method name
Improves security (prevents substring matches), maintainability (uses framework APIs with explicit scope), and follows Ruby conventions.
With the exception of specific 'disallowed paths' (i.e. `new_user_session_path` for `after_sign_in_path_for` vs `new_user_session_path` and `new_user_registration_path` for `after_sign_up_path_for`, these helpers are otherwise identical. This change refactors the code so that these helpers better adhere to DRY principles. We introduce `def after_auth_path(disallowed_paths:)`. `after_sign_in_path_for` and `after_sign_up_path_for` each call `after_auth_path` with an array containing their aforementioned disallowed paths. Some small additional refactoring is also performed. `return root_path if request.referer.nil? || from_external_domain?` allows for possible early return before assigning `referer_path`. Also, repeated `referer_path.eql?(path)` statements have been replaced with a single `disallowed_paths.include?(referer_path)` check.
Amongst its changes, this commit removes `warden.authenticate!(scope: :user)`. When warden.authenticate! is invoked, the OAuth flow is preserved while the user signs in. However, our current custom implementation of ApplicationController#after_sign_in_path_for only returns `stored_location_for(resource)` for the `oauth/authorize` path. - We could further modify after_sign_in_path_for to also return `stored_location_for(resource)` for the `oauth/applications` path. However, it is simple enough for a super admin to navigate to `oauth/applications` via the admin panel.
- Edit plan query to check for user role to prevent eager loading - Add set_complete_param function that sets complete flag according to its value if its set to true in the API call
- Edit initialize to include complete flag, which is set to false as default - If flag is true in call, call fetch_all_q_and_a - Add fetch_all_q_and_a function that fetches questions and answers Update plan_presenter
- Pre-fetch the answers, questions, and sections as part of the initial plans query (but only when we are in fact fetching all of the questions and answers). - This speeds up the mean request time by 25%
Generate and apply migration to remove NOT NULL constraint from `oauth_applications.redirect_uri`. This works in conjunction with `allow_blank_redirect_uri true` in `config/initializers/doorkeeper.rb`, allowing us to save `OAuthApplications` with blank `redirect_uri` values for URI-less flows like client_credentials or password grant flows. NOTE: Applications using `authorization_code` (or other flows that require a redirect URI) must still have a non-blank `redirect_uri`. Otherwise, `/oauth/authorize` requests with `response_type=code` will fail. More info is available here: https://github.com/doorkeeper-gem/doorkeeper/wiki/Allow-blank-redirect-URI-for-Applications
- Enable `hash_application_secrets` and `hash_token_secrets` in Doorkeeper initializer for improved security. - This stores application secrets and access tokens as hashes in the database, reducing risk if the database is compromised. - Note: `hash_token_secrets` is incompatible with `reuse_access_token`, so token reuse is now disabled/removed (see warning in Doorkeeper docs). - For more details, see: https://doorkeeper.gitbook.io/guides/security/token-and-application-secrets
Prior to this change, `LocaleService.default_locale` was always assigned to `json.language`. - Now we use `plan.owner&.language&.abbreviation` (or `LocaleService.default_locale` as a fallback)
- Updated `it 'includes the :language'` test to reflect changes made in b238c8128e0fefb9ea25cc7528e5be2c499d9318. - Updated `Api::V1` references within spec files with `Api::V2`
NOTE: The prior comment stated the following: # Attach the first data_curation role as the data_contact, otherwise # add the contributor to the contributors array However, the contributor was ALWAYS added to the contributors array
Addresses the following Bullet warning: user: aaron GET /api/v2/templates USE eager loading detected Identifier => [:identifier_scheme] Add to your query: .includes([:identifier_scheme]) Call stack /home/aaron/Documents/GitHub/roadmap/app/presenters/api/v2/org_presenter.rb:9:in `block in affiliation_id' /home/aaron/Documents/GitHub/roadmap/app/presenters/api/v2/org_presenter.rb:9:in `affiliation_id' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/orgs/_show.json.jbuilder:11:in `block in _app_views_api_v__orgs__show_json_jbuilder__1237609272914133563_48080' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/orgs/_show.json.jbuilder:10:in `_app_views_api_v__orgs__show_json_jbuilder__1237609272914133563_48080' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/templates/index.json.jbuilder:16:in `block (3 levels) in _app_views_api_v__templates_index_json_jbuilder___4313931085157922975_47640' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/templates/index.json.jbuilder:15:in `block (2 levels) in _app_views_api_v__templates_index_json_jbuilder___4313931085157922975_47640' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/templates/index.json.jbuilder:8:in `block in _app_views_api_v__templates_index_json_jbuilder___4313931085157922975_47640' /home/aaron/Documents/GitHub/roadmap/app/views/api/v2/templates/index.json.jbuilder:5:in `_app_views_api_v__templates_index_json_jbuilder___4313931085157922975_47640' /home/aaron/Documents/GitHub/roadmap/app/controllers/api/v2/templates_controller.rb:13:in `index'
This change further addresses Bullet warnings that were earlier addressed in commit 6ed969b
This change updates `Api::V2::PlansController#show` to use the `.for_api_v2` scope, and applies eager loading of answers (`.includes(answers: { question: :section })`) based on the `complete` param--making the `show` action consistent with `index`.
A new `plans_scope` helper DRYs up the controller by centralizing the shared scope and eager loading logic. Since `.for_api_v2` already filters by `.where(roles: { user_id: user_id, active: true })`, only a presence check is now required in the policy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters