Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication between Third Party Apps and the ELN #1353

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
091bb30
Communication with Third Party App
Konrad1991 Jun 19, 2023
f008d73
removed the namespaces in the grape api file third_party_apps.rb.
Konrad1991 Jun 26, 2023
c6b3956
Combined three migration files into one.
Konrad1991 Jun 27, 2023
80caa1a
- json web token is now created using the model class json_web_token
Konrad1991 Jul 7, 2023
ede8e53
- During creation of a new third party app entry the check whether th…
Konrad1991 Jul 7, 2023
39d2824
- the token for the third party app is now cached
Konrad1991 Jul 13, 2023
4fe35ba
Merge remote-tracking branch 'upstream/main' into Third-Party-Apps
Konrad1991 Jul 19, 2023
236f34a
- The token which is generated for the third party app is now cached
Konrad1991 Jul 19, 2023
db2a9ce
Merge remote-tracking branch 'local/Third-Party-Apps' into Third-Part…
Konrad1991 Jul 19, 2023
f07eca3
run ESLINT and removed errors for TPA stuff
Konrad1991 Oct 16, 2023
ad49f4c
Merge remote-tracking branch 'local/Third-Party-Apps' into Third-Part…
Konrad1991 Oct 16, 2023
53b59de
enabled file-cache in development and production environment.
Konrad1991 Oct 16, 2023
2f37f8b
removed bug of empty ThirdPartyApp array when no TPA's exists
Konrad1991 Oct 30, 2023
63a3ee2
Removed error which occured due to nil instead of an empty array for …
Konrad1991 Oct 30, 2023
faf6f62
the public url of the ELN is send now as query parameter to third par…
Konrad1991 Jan 2, 2024
a38559d
the public url is now passed as query parameters to the third party app
Konrad1991 Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/api/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def authenticate!
def is_public_request?
request.path.start_with?(
'/api/v1/public/',
'/api/v1/public_third_party_app/',
'/api/v1/chemscanner/',
'/api/v1/chemspectra/',
'/api/v1/ketcher/layout',
Expand Down Expand Up @@ -180,7 +181,7 @@ def to_json_camel_case(val)
mount Chemotion::ConverterAPI
mount Chemotion::AttachableAPI
mount Chemotion::SampleTaskAPI
mount Chemotion::ChemicalAPI
mount Chemotion::ThirdPartyAppAPI
mount Chemotion::CalendarEntryAPI

add_swagger_documentation(info: {
Expand Down
233 changes: 233 additions & 0 deletions app/api/chemotion/third_party_app_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# frozen_string_literal: true

module Chemotion
# Publish-Subscription MessageAPI
class ThirdPartyAppAPI < Grape::API
helpers do
def extract_values(payload)
att_id = payload[0]['attID']&.to_i
user_id = payload[0]['userID']&.to_i
name_third_party_app = payload[0]['nameThirdPartyApp']&.to_s
[att_id, user_id, name_third_party_app]
end

def decode_token(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base) unless token.nil?
error!('401 Unauthorized', 401) if payload&.length&.zero?
extract_values(payload)
end

def verify_token(token)
payload = decode_token(token)
@attachment = Attachment.find_by(id: payload[0])
@user = User.find_by(id: payload[1])
error!('401 Unauthorized', 401) if @attachment.nil? || @user.nil?
end

def perform_download(token_cached, attachment)
if token_cached.counter <= 3
attachment.read_file
else
error!('Too many requests with this token', 403)
end
end

def update_cache(cache_key, token_cached)
error!('Invalid token', 403) if token_cached.nil?
token_cached.counter = token_cached.counter + 1
Rails.cache.write(cache_key, token_cached)
end

def download_third_party_app(token)
content_type 'application/octet-stream'
verify_token(token)
payload = decode_token(token)
@attachment = Attachment.find_by(id: payload[0])
@user = User.find_by(id: payload[1])
header['Content-Disposition'] = "attachment; filename=#{@attachment.filename}"
env['api.format'] = :binary
cache_key = "token/#{payload[0]}/#{payload[1]}/#{payload[2]}"
token_cached = Rails.cache.read(cache_key)
update_cache(cache_key, token_cached)
perform_download(token_cached, @attachment)
end

def upload_third_party_app(token, file_name, file, file_type)
payload = decode_token(token)
cache_key = "token/#{payload[0]}/#{payload[1]}/#{payload[2]}"
token_cached = Rails.cache.read(cache_key)
update_cache(cache_key, token_cached)
if token_cached.counter > 30
error!('To many request with this token', 403)
else
attachment = Attachment.find_by(id: payload[0])
new_attachment = Attachment.new(attachable: attachment.attachable,
created_by: attachment.created_by,
created_for: attachment.created_for,
content_type: file_type)
File.open(file[:tempfile].path, 'rb') do |f|
new_attachment.update(file_path: f, filename: file_name)
end
{ message: 'File uploaded successfully' }
end
end

def encode_token(payload, name_third_party_app)
cache_key = cache_key_for_encoded_token(payload)
Rails.cache.fetch(cache_key, expires_in: 48.hours) do
token = JsonWebToken.encode(payload, 48.hours.from_now)
CachedTokenThirdPartyApp.new(token, 0, name_third_party_app)
end
end

def cache_key_for_encoded_token(payload)
"encoded_token/#{payload[:attID]}/#{payload[:userID]}/#{payload[:nameThirdPartyApp]}"
end
end

namespace :public_third_party_app do
desc 'download file from third party app'
params do
requires :token, type: String, desc: 'Token for authentication'
end
get '/download' do
error!('401 Unauthorized', 401) if params[:token].nil?
download_third_party_app(params[:token])
end

desc 'Upload file from third party app'
params do
requires :token, type: String, desc: 'Token for authentication'
requires :attachmentName, type: String, desc: 'Name of the attachment'
requires :fileType, type: String, desc: 'Type of the file'
end
post '/upload' do
error!('401 Unauthorized', 401) if params[:token].nil?
error!('401 Unauthorized', 401) if params[:attachmentName].nil?
error!('401 Unauthorized', 401) if params[:fileType].nil?
verify_token(params[:token])
upload_third_party_app(params[:token],
params[:attachmentName],
params[:file],
params[:file_type])
end
end

resource :third_party_apps_administration do
before do
error(401) unless current_user.is_a?(Admin)
end

desc 'check that name is unique'
params do
requires :name
end
post '/name_unique' do
declared(params, include_missing: false)
result = ThirdPartyApp.all_names
if result.nil?
{ message: 'Name is unique' }
elsif ThirdPartyApp.all_names.exclude?(params[:name])
{ message: 'Name is unique' }
else
{ message: 'Name is not unique' }
end.to_json
rescue ActiveRecord::RecordInvalid
error!('Unauthorized. User has to be admin.', 401)
end

desc 'create new third party app entry'
params do
requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.'
requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.'
end
post '/new_third_party_app' do
declared(params, include_missing: false)
ThirdPartyApp.create!(IPAddress: params[:IPAddress], name: params[:name])
status 201
rescue ActiveRecord::RecordInvalid
error!('Unauthorized. User has to be admin.', 401)
end

desc 'update a third party app entry'
params do
requires :id, type: String, desc: 'The id of the app which should be updated'
requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.'
requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.'
end
post '/update_third_party_app' do
declared(params, include_missing: false)
entry = ThirdPartyApp.find(params[:id])
entry.update!(IPAddress: params[:IPAddress], name: params[:name])
status 201
rescue ActiveRecord::RecordInvalid
error!('Unauthorized. User has to be admin.', 401)
end

desc 'delete third party app entry'
params do
requires :id, type: String, desc: 'The id of the app which should be deleted'
end
post '/delete_third_party_app' do
id = params[:id].to_i
ThirdPartyApp.delete(id)
status 201
rescue ActiveRecord::RecordInvalid
error!('Unauthorized. User has to be admin.', 401)
end
end

resource :third_party_apps do
desc 'Find all thirdPartyApps'
get 'all' do
ThirdPartyApp.all
end

desc 'get third party app by id'
params do
requires :id, type: String, desc: 'The id of the app'
end
get 'get_by_id' do
ThirdPartyApp.find(params[:id])
end

desc 'get ip address of third party app'
params do
requires :name, type: String, desc: 'The name of the app for which the ip address should be get.'
end
get 'IP' do
tpa = ThirdPartyApp.find_by(name: params[:name])
return tpa.IPAddress if tpa

error_msg = "Third party app with ID: #{id} not found"
{ error: error_msg }
end

desc 'get public ip address of ELN'
get 'public_IP' do
uri = URI.parse(ENV['PUBLIC_URL'] || 'http://localhost:3000')
end

desc 'create token for use in download public_api'
params do
requires :attID, type: String, desc: 'Attachment ID'
requires :userID, type: String, desc: 'User ID'
requires :nameThirdPartyApp, type: String, desc: 'name of the third party app'
end
get 'Token' do
cache_key = "token/#{params[:attID]}/#{params[:userID]}/#{params[:nameThirdPartyApp]}"
payload = { attID: params[:attID], userID: params[:userID], nameThirdPartyApp: params[:nameThirdPartyApp] }
cached_token = encode_token(payload, params[:nameThirdPartyApp])
Rails.cache.write(cache_key, cached_token, expires_in: 48.hours)
cached_token.token
end
end

resource :names do
desc 'Find all names of all third party app'
get 'all' do
ThirdPartyApp.all_names
end
end
end
end
9 changes: 9 additions & 0 deletions app/api/entities/third_party_app_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module Entities
class ThirdPartyAppEntity < Grape::Entity
expose :ip_address
expose :name
expose :password
end
end
11 changes: 11 additions & 0 deletions app/models/cached_token_third_party_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class CachedTokenThirdPartyApp
attr_accessor :token, :counter, :name_tpa

def initialize(token, counter, name_tpa)
@token = token
@counter = counter
@name_tpa = name_tpa
end
end
14 changes: 14 additions & 0 deletions app/models/third_party_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class ThirdPartyApp < ApplicationRecord
def self.all_names
return nil if ThirdPartyApp.count.zero?

entries = ThirdPartyApp.all
names = []
entries.each do |e|
names << e.name
end
names
end
end
15 changes: 15 additions & 0 deletions app/packs/src/apps/admin/AdminHome.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SegmentElementAdmin from 'src/apps/admin/SegmentElementAdmin';
import DatasetElementAdmin from 'src/apps/admin/DatasetElementAdmin';
import DelayedJobs from 'src/apps/admin/DelayedJobs';
// import TemplateManagement from 'src/apps/admin/TemplateManagement';
import ThirdPartyApp from 'src/apps/admin/ThirdPartyApp';

class AdminHome extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -74,6 +75,10 @@ class AdminHome extends React.Component {
return this.renderTemplateManagement();
} else if (pageIndex === 13) {
return this.renderDelayedJobs();
} else if (pageIndex === 14) {
return this.renderConverterAdmin();
} else if (pageIndex === 15) {
return this.renderThirdPartyApp();
}
return (<div />);
}
Expand Down Expand Up @@ -102,12 +107,22 @@ class AdminHome extends React.Component {
<NavItem eventKey={5}>Load OLS Terms</NavItem>
{/* <NavItem eventKey={12}>Report-template Management</NavItem> */}
<NavItem eventKey={13}>Delayed Jobs </NavItem>
<NavItem eventKey={15}>Third Party Apps </NavItem>
</Nav>
</Col>
</div>
);
}

renderThirdPartyApp() {
const { contentClassName } = this.state;
return (
<Col className={contentClassName} >
<ThirdPartyApp />
</Col>
);
}

renderDashboard() {
const { contentClassName } = this.state;
return (
Expand Down
Loading