Skip to content

Commit 8e11ae8

Browse files
authored
Merge pull request #1184 from ElixirTeSS/orcid-integration
ORCID authentication
2 parents 3a06b5e + fdbcf98 commit 8e11ae8

30 files changed

Lines changed: 726 additions & 21 deletions
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

app/controllers/application_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def handle_error(status_code = 500, message = nil)
4848
status_code = status_code.to_i
4949
@message = message
5050
respond_to do |format|
51-
format.html { render 'static/error', status: status_code }
51+
format.html { render 'static/error', status: status_code }
5252
format.json { render json: { error: { message: message, code: status_code } }, status: status_code }
5353
format.json_api { render json: { error: { message: message, code: status_code } }, status: status_code }
5454
format.any { head status_code }
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
class OrcidController < ApplicationController
2+
before_action :authenticate_user!
3+
before_action :set_oauth_client, only: [:authenticate, :callback]
4+
5+
# Faraday::ParsingError occurs in rack-oauth2 if the response does not contain JSON
6+
rescue_from(Rack::OAuth2::Client::Error, Faraday::ParsingError) do
7+
handle_error(:unprocessable_entity, t('orcid.error'))
8+
end
9+
10+
def authenticate
11+
redirect_to @oauth2_client.authorization_uri(scope: '/authenticate'), allow_other_host: true
12+
end
13+
14+
def callback
15+
@oauth2_client.authorization_code = params[:code]
16+
token = Rack::OAuth2::AccessToken::Bearer.new(access_token: @oauth2_client.access_token!)
17+
orcid = token.access_token&.raw_attributes['orcid']
18+
respond_to do |format|
19+
profile = current_user.profile
20+
if orcid.present?
21+
if profile.authenticate_orcid(orcid)
22+
flash[:notice] = t('orcid.authentication_success')
23+
else
24+
flash[:error] = profile.errors.full_messages.join(', ')
25+
end
26+
else
27+
flash[:error] = t('orcid.authentication_failure')
28+
end
29+
format.html { redirect_to current_user }
30+
end
31+
end
32+
33+
private
34+
35+
def set_oauth_client
36+
config = Rails.application.config.secrets.orcid
37+
@oauth2_client ||= Rack::OAuth2::Client.new(
38+
identifier: config[:client_id],
39+
secret: config[:secret],
40+
redirect_uri: config[:redirect_uri].presence || orcid_callback_url,
41+
authorization_endpoint: '/oauth/authorize',
42+
token_endpoint: '/oauth/token',
43+
host: config[:host].presence || (Rails.env.production? ? 'orcid.org' : 'sandbox.orcid.org')
44+
)
45+
end
46+
end

app/helpers/users_helper.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,19 @@ def self.user_profile_resource_limit
55
30
66
end
77

8+
def orcid_link(profile, **opts)
9+
content_tag(:span) do
10+
if profile.orcid_authenticated?
11+
concat image_tag('ORCID-iD_icon_vector.svg', size: 16)
12+
concat ' '
13+
concat external_link(profile.orcid, profile.orcid_url, **opts)
14+
elsif profile.orcid.present?
15+
concat image_tag('ORCID-iD_icon_unauth_vector.svg', size: 16)
16+
concat ' '
17+
concat external_link(profile.orcid, profile.orcid_url, **opts)
18+
concat ' (Unauthenticated)'
19+
end
20+
end
21+
end
22+
823
end

app/models/profile.rb

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ def full_name
2525
"#{firstname} #{surname}".strip
2626
end
2727

28+
def orcid_url
29+
return nil if orcid.blank?
30+
"#{OrcidValidator::ORCID_PREFIX}#{orcid}"
31+
end
32+
2833
def merge(*others)
2934
Profile.transaction do
3035
attrs = attributes
@@ -43,16 +48,27 @@ def merge(*others)
4348
end
4449
end
4550

51+
def authenticate_orcid(orcid)
52+
existing = Profile.where(orcid: orcid, orcid_authenticated: true)
53+
self.orcid = orcid
54+
self.orcid_authenticated = true
55+
out = self.save
56+
57+
if out
58+
existing.each do |profile|
59+
next if profile == self
60+
profile.update_column(:orcid_authenticated, false)
61+
end
62+
end
63+
64+
out
65+
end
66+
4667
private
4768

4869
def normalize_orcid
4970
return if orcid.blank?
50-
self.orcid = orcid.strip
51-
if orcid =~ OrcidValidator::ORCID_ID_REGEX
52-
self.orcid = "#{OrcidValidator::ORCID_PREFIX}#{orcid}"
53-
elsif orcid.start_with?(OrcidValidator::ORCID_DOMAIN_REGEX)
54-
self.orcid = orcid.sub(OrcidValidator::ORCID_DOMAIN_REGEX, OrcidValidator::ORCID_PREFIX)
55-
end
71+
self.orcid = orcid.strip.sub(OrcidValidator::ORCID_DOMAIN_REGEX, '')
5672
end
5773

5874
def check_public

app/views/trainers/partials/_trainer_schemaorg.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<%= content_tag 'span', langs, { itemprop: 'knowsLanguage', content: langs, class: 'schemaorg-element' } %>
1414
<% end %>
1515
<%= content_tag :span, trainer.website, { itemprop: 'url', content: trainer.website, class: 'schemaorg-element' } %>
16-
<%= content_tag :span, trainer.orcid, { itemprop: 'identifier', content: trainer.orcid, class: 'schemaorg-element' } %>
16+
<%= content_tag :span, trainer.orcid_url, { itemprop: 'identifier', content: trainer.orcid_url, class: 'schemaorg-element' } %>
1717
<% if trainer.image_url and not trainer.image_url.empty? %>
1818
<%= content_tag :span, trainer.image_url, { itemprop: 'image', content: trainer.image_url, class: 'shemaorg-element' } %>
1919
<% end %>

app/views/trainers/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<!-- Field: orcid -->
2222
<% if @trainer.orcid.present? %>
2323
<div class="url-wrap">
24-
<%= external_link @trainer.orcid, @trainer.orcid, class: 'h5', target: '_blank', rel: 'noopener', track: true %>
24+
<%= orcid_link(@trainer, class: 'h5', track: true) %>
2525
</div>
2626
<% end %>
2727

app/views/users/show.html.erb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
<% if @user.profile.orcid.blank? %>
5151
<span class="empty">None specified</span>
5252
<% else %>
53-
<%= link_to @user.profile.orcid, @user.profile.orcid, rel: 'nofollow', target: '_blank' %>
53+
<%= orcid_link(@user.profile) %>
54+
<% if current_user == @user && !@user.profile.orcid_authenticated? %>
55+
<%= button_to t('orcid.authenticate'), authenticate_orcid_path, class: 'btn btn-default' %>
56+
<% end %>
5457
<% end %>
5558
</p>
5659

config/locales/en.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,8 @@ en:
839839
422: "The request you sent was well-formed but the change you wanted was rejected (422 Unprocessable Entity)."
840840
404: "The requested page could not be found - you may have mistyped the address or the page may have moved (404 Not Found)."
841841
406: "The requested format is not available (406 Not Acceptable)."
842+
messages:
843+
orcid_taken: 'has already been linked to another profile.'
842844
warnings:
843845
link_broken: >
844846
%{site_name} has been unable to access this %{resource_type}'s URL since %{fail_date} - the page may have been moved.
@@ -1114,3 +1116,8 @@ en:
11141116
title: What are spaces?
11151117
description: |
11161118
Spaces are customizable, community-managed sub-portals within %{site_name}, each with their own catalogue of training content.
1119+
orcid:
1120+
error: 'An error occurred whilst trying to authenticate your ORCID.'
1121+
authenticate: 'Authenticate your ORCID'
1122+
authentication_success: 'You have successfully authenticated your ORCID.'
1123+
authentication_failure: 'Failed to authenticate your ORCID.'

0 commit comments

Comments
 (0)