Skip to content

Commit

Permalink
Refactor ROR API query to route through server to protect user IP
Browse files Browse the repository at this point in the history
  • Loading branch information
whomingbird committed Feb 26, 2025
1 parent 04cbde3 commit 35adcc5
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 17 deletions.
44 changes: 27 additions & 17 deletions app/assets/javascripts/institution-ror-typeahead.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
var ROR_API_URL = "https://api.ror.org/organizations";


function toggleUserInput(disabled) {
const action = disabled ? 'addClass' : 'removeClass';
const elements = [
Expand Down Expand Up @@ -29,22 +26,29 @@ function extractRorId(rorUrl) {
}

function fetchRorData(rorId) {
var url = ROR_API_URL + '/' + rorId;

//use /institutions/ror_search endpoint instead of the direct ROR API
var url = `/institutions/ror_search?ror_id=${encodeURIComponent(rorId)}`;

fetch(url)
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.errors ? err.errors[0] : "Unknown error occurred");
throw new Error(err.error || "Unknown error occurred");
});
}
return response.json();
})
.then(data => {
$j('#ror-response').html(JSON.stringify(data, undefined, 4));
$j('#institution_title').val(data.name);
$j('#institution_city').val(data.addresses[0]['city']);
$j('#institution_country').val(data.country.country_code);
$j('#institution_ror_id').val(extractRorId(data.id));
if (!data || data.error) {
throw new Error(data.error || "Invalid response from server");
}

$j('#ror-response').html(JSON.stringify(data, null, 4));
$j('#institution_title').val(data.name || 'N/A');
$j('#institution_city').val(data.addresses?.[0]?.city || 'N/A');
$j('#institution_country').val(data.country?.country_code || 'N/A');
$j('#institution_ror_id').val(extractRorId(data.id) || '');
$j('#institution_web_page').val(data.links?.[0] || 'N/A');
$j('#ror-error-message').text('').hide();
$j('#institution_ror_id').removeClass("field_with_errors");
Expand All @@ -60,17 +64,23 @@ function fetchRorData(rorId) {

// ROR API source logic
function rorQuerySource(query, processSync, processAsync) {
if (query.length < 3) {
return processAsync([]); // Trigger only for 3+ characters
if (query.length < 4) {
return processAsync([]);
}
var url = ROR_API_URL + '?query=' + encodeURIComponent(query);

//use /institutions/ror_search endpoint instead of the direct ROR API
var url = `/institutions/ror_search?query=${encodeURIComponent(query)}`;

return $j.ajax({
url: url,
type: 'GET',
dataType: 'json',
success: function (json) {
const orgs = json.items;
const orgs = json.items || [];
return processAsync(orgs);
},
error: function (xhr, status, error) {
processAsync([]);
}
});
}
Expand Down Expand Up @@ -98,7 +108,7 @@ function rorSuggestionTemplate(data) {
altNames += label.label + ", ";
});
}
altNames = altNames.replace(/,\s*$/, ""); // Trim trailing comma
altNames = altNames.replace(/,\s*$/, "");
return `
<div>
<p>
Expand All @@ -121,7 +131,7 @@ function initializeLocalInstitutions(endpoint = '/institutions/typeahead.json',
wildcard: '%QUERY',
cache: cache,
transform: function (response) {
return response.results; // Ensure response is structured correctly
return response.results;
}
}
});
Expand Down Expand Up @@ -157,7 +167,7 @@ $j(document).ready(function () {
{
hint: true,
highlight: true,
minLength: 1 // Start suggestions after typing at least 1 character
minLength: 4
},
// First Dataset: Local Institutions
{
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/institutions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'ror/client'

class InstitutionsController < ApplicationController
include Seek::IndexPager
include CommonSweepers
Expand Down Expand Up @@ -115,6 +117,24 @@ def request_all
end
end


def ror_search
client = RORClient::Client.new
if params[:query].present?
response = client.query_name(params[:query])
elsif params[:ror_id].present?
response = client.fetch_by_id(params[:ror_id])
else
render json: { error: 'Missing ROR ID' }, status: 400 and return
end

if response.key?(:error)
render json: response, status: 500
else
render json: response
end
end

# request all institutions, but specific to the sharing form which expects an array
def request_all_sharing_form
institution_list = Institution.order(:id).collect{ |institution| [institution.title, institution.id] }
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@
get :request_all
get :request_all_sharing_form
get :typeahead
get :ror_search
end
resources :people, :programmes, :projects, :specimens, only: [:index]
end
Expand Down
32 changes: 32 additions & 0 deletions lib/ror/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module RORClient
class Client
ENDPOINT = 'https://api.ror.org'.freeze

def initialize(endpoint = ENDPOINT)
@endpoint = RestClient::Resource.new(endpoint)
end

# Search for an institution by name
def query_name(query)
encoded_query = URI.encode_www_form_component(query)
request("organizations?query=#{encoded_query}")
end

# Fetch institution details by ROR ID
def fetch_by_id(ror_id)
request("organizations/#{ror_id}")
end

private

# Generic method to make requests and handle errors
def request(path)
response = @endpoint[path].get(accept: 'application/json')
JSON.parse(response.body)
rescue RestClient::ExceptionWithResponse => e
{ error: "ROR API error: #{e.response}" }
rescue StandardError => e
{ error: "Unexpected error: #{e.message}" }
end
end
end

0 comments on commit 35adcc5

Please sign in to comment.