diff --git a/Gemfile b/Gemfile index a82336d..4444b9f 100644 --- a/Gemfile +++ b/Gemfile @@ -12,4 +12,5 @@ gem "sprockets-rails" # gem "debug", ">= 1.0.0" gem "atomic_lti", "1.5.0" -gem 'atomic_tenant', "1.2.0" \ No newline at end of file +gem "atomic_tenant", "1.2.0" +gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index 160a727..47fbe49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,93 +1,115 @@ PATH remote: . specs: - atomic_admin (1.0.0) + atomic_admin (2.0.0.beta.4) rails (>= 7.0, < 9.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.7) - actionpack (= 7.0.8.7) - activesupport (= 7.0.8.7) + actioncable (7.2.2.1) + actionpack (= 7.2.2.1) + activesupport (= 7.2.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.7) - actionpack (= 7.0.8.7) - activejob (= 7.0.8.7) - activerecord (= 7.0.8.7) - activestorage (= 7.0.8.7) - activesupport (= 7.0.8.7) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.0.8.7) - actionpack (= 7.0.8.7) - actionview (= 7.0.8.7) - activejob (= 7.0.8.7) - activesupport (= 7.0.8.7) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.7) - actionview (= 7.0.8.7) - activesupport (= 7.0.8.7) - rack (~> 2.0, >= 2.2.4) + zeitwerk (~> 2.6) + actionmailbox (7.2.2.1) + actionpack (= 7.2.2.1) + activejob (= 7.2.2.1) + activerecord (= 7.2.2.1) + activestorage (= 7.2.2.1) + activesupport (= 7.2.2.1) + mail (>= 2.8.0) + actionmailer (7.2.2.1) + actionpack (= 7.2.2.1) + actionview (= 7.2.2.1) + activejob (= 7.2.2.1) + activesupport (= 7.2.2.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.2.1) + actionview (= 7.2.2.1) + activesupport (= 7.2.2.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.7) - actionpack (= 7.0.8.7) - activerecord (= 7.0.8.7) - activestorage (= 7.0.8.7) - activesupport (= 7.0.8.7) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.2.1) + actionpack (= 7.2.2.1) + activerecord (= 7.2.2.1) + activestorage (= 7.2.2.1) + activesupport (= 7.2.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.7) - activesupport (= 7.0.8.7) + actionview (7.2.2.1) + activesupport (= 7.2.2.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.7) - activesupport (= 7.0.8.7) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.2.1) + activesupport (= 7.2.2.1) globalid (>= 0.3.6) - activemodel (7.0.8.7) - activesupport (= 7.0.8.7) - activerecord (7.0.8.7) - activemodel (= 7.0.8.7) - activesupport (= 7.0.8.7) - activestorage (7.0.8.7) - actionpack (= 7.0.8.7) - activejob (= 7.0.8.7) - activerecord (= 7.0.8.7) - activesupport (= 7.0.8.7) + activemodel (7.2.2.1) + activesupport (= 7.2.2.1) + activerecord (7.2.2.1) + activemodel (= 7.2.2.1) + activesupport (= 7.2.2.1) + timeout (>= 0.4.0) + activestorage (7.2.2.1) + actionpack (= 7.2.2.1) + activejob (= 7.2.2.1) + activerecord (= 7.2.2.1) + activesupport (= 7.2.2.1) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.7) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (7.2.2.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) atomic_lti (1.5.0) pg (~> 1.3) rails (~> 7.0) atomic_tenant (1.2.0) atomic_lti (~> 1.3) rails (~> 7.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) builder (3.3.0) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) crass (1.0.6) - date (3.3.3) - erubi (1.13.0) - globalid (1.1.0) - activesupport (>= 5.0) - i18n (1.14.6) + csv (3.3.4) + date (3.4.1) + drb (2.2.1) + erubi (1.13.1) + globalid (1.2.1) + activesupport (>= 6.1) + httparty (0.23.1) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + i18n (1.14.7) concurrent-ruby (~> 1.0) - loofah (2.23.1) + io-console (0.8.0) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + logger (1.7.0) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -95,79 +117,104 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) - method_source (1.0.0) + marcel (1.0.4) mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.25.4) - net-imap (0.3.7) + minitest (5.25.5) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + net-imap (0.5.8) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.5.1) net-protocol - nio4r (2.5.9) - nokogiri (1.17.1-x86_64-darwin) + nio4r (2.7.4) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.17.1-x86_64-linux) + nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) - pg (1.5.3) + nokogiri (1.18.8-x86_64-linux) + racc (~> 1.4) + pg (1.5.9) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + psych (5.2.4) + date + stringio racc (1.8.1) - rack (2.2.10) - rack-test (2.1.0) + rack (3.1.14) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.7) - actioncable (= 7.0.8.7) - actionmailbox (= 7.0.8.7) - actionmailer (= 7.0.8.7) - actionpack (= 7.0.8.7) - actiontext (= 7.0.8.7) - actionview (= 7.0.8.7) - activejob (= 7.0.8.7) - activemodel (= 7.0.8.7) - activerecord (= 7.0.8.7) - activestorage (= 7.0.8.7) - activesupport (= 7.0.8.7) + rackup (2.2.1) + rack (>= 3) + rails (7.2.2.1) + actioncable (= 7.2.2.1) + actionmailbox (= 7.2.2.1) + actionmailer (= 7.2.2.1) + actionpack (= 7.2.2.1) + actiontext (= 7.2.2.1) + actionview (= 7.2.2.1) + activejob (= 7.2.2.1) + activemodel (= 7.2.2.1) + activerecord (= 7.2.2.1) + activestorage (= 7.2.2.1) + activesupport (= 7.2.2.1) bundler (>= 1.15.0) - railties (= 7.0.8.7) + railties (= 7.2.2.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.0.8.7) - actionpack (= 7.0.8.7) - activesupport (= 7.0.8.7) - method_source + railties (7.2.2.1) + actionpack (= 7.2.2.1) + activesupport (= 7.2.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rake (13.0.6) - sprockets (4.2.0) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + rdoc (6.13.1) + psych (>= 4.0.0) + reline (0.6.1) + io-console (~> 0.5) + securerandom (0.4.1) + sprockets (4.2.2) concurrent-ruby (~> 1.0) + logger rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.6.3) - mini_portile2 (~> 2.8.0) - sqlite3 (1.6.3-x86_64-darwin) - thor (1.2.2) - timeout (0.4.0) + sqlite3 (2.6.0-arm64-darwin) + sqlite3 (2.6.0-x86_64-darwin) + sqlite3 (2.6.0-x86_64-linux) + stringio (3.1.7) + thor (1.3.2) + timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - websocket-driver (0.7.6) + useragent (0.16.11) + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.11) + zeitwerk (2.6.18) PLATFORMS + arm64-darwin-21 + arm64-darwin-23 x86_64-darwin-22 x86_64-linux @@ -175,6 +222,7 @@ DEPENDENCIES atomic_admin! atomic_lti (= 1.5.0) atomic_tenant (= 1.2.0) + httparty sprockets-rails sqlite3 diff --git a/app/controllers/atomic_admin/application_controller.rb b/app/controllers/atomic_admin/api/admin/v0/admin_controller.rb similarity index 56% rename from app/controllers/atomic_admin/application_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/admin_controller.rb index df0cd7a..3a9599a 100644 --- a/app/controllers/atomic_admin/application_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/admin_controller.rb @@ -1,10 +1,10 @@ -module AtomicAdmin +module AtomicAdmin::Api::Admin::V0 BASE_CONTROLLER = if AtomicAdmin.authenticating_base_controller_class AtomicAdmin.authenticating_base_controller_class.constantize else - AtomicAdmin::AuthenticatingApplicationController + AtomicAdmin::Api::Admin::V0::AuthenticatingApplicationController end - class ApplicationController < BASE_CONTROLLER + class AdminController < BASE_CONTROLLER end end diff --git a/app/controllers/atomic_admin/atomic_lti_install_controller.rb b/app/controllers/atomic_admin/api/admin/v0/atomic_lti_install_controller.rb similarity index 88% rename from app/controllers/atomic_admin/atomic_lti_install_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/atomic_lti_install_controller.rb index d17e8d0..d76e12e 100644 --- a/app/controllers/atomic_admin/atomic_lti_install_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/atomic_lti_install_controller.rb @@ -1,5 +1,5 @@ -module AtomicAdmin - class AtomicLtiInstallController < ApplicationController +module AtomicAdmin::Api::Admin::V0 + class AtomicLtiInstallController < AdminController def install_params params.permit(:iss, :client_id) end diff --git a/app/controllers/atomic_admin/atomic_lti_platform_controller.rb b/app/controllers/atomic_admin/api/admin/v0/atomic_lti_platform_controller.rb similarity index 90% rename from app/controllers/atomic_admin/atomic_lti_platform_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/atomic_lti_platform_controller.rb index f23da1e..4b1ead5 100644 --- a/app/controllers/atomic_admin/atomic_lti_platform_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/atomic_lti_platform_controller.rb @@ -1,5 +1,5 @@ -module AtomicAdmin - class AtomicLtiPlatformController < ApplicationController +module AtomicAdmin::Api::Admin::V0 + class AtomicLtiPlatformController < AdminController def platform_params params.permit(:iss, :jwks_url, :token_url, :oidc_url) end diff --git a/app/controllers/atomic_admin/atomic_tenant_client_id_strategy_controller.rb b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_client_id_strategy_controller.rb similarity index 94% rename from app/controllers/atomic_admin/atomic_tenant_client_id_strategy_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/atomic_tenant_client_id_strategy_controller.rb index 5f540cb..83f8c69 100644 --- a/app/controllers/atomic_admin/atomic_tenant_client_id_strategy_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_client_id_strategy_controller.rb @@ -1,5 +1,5 @@ -module AtomicAdmin - class AtomicTenantClientIdStrategyController < ApplicationController +module AtomicAdmin::Api::Admin::V0 + class AtomicTenantClientIdStrategyController < AdminController if AtomicAdmin.client_id_strategy_before_action.present? before_action AtomicAdmin.client_id_strategy_before_action, only: [:create, :update] diff --git a/app/controllers/atomic_admin/atomic_tenant_deployment_controller.rb b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_deployment_controller.rb similarity index 95% rename from app/controllers/atomic_admin/atomic_tenant_deployment_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/atomic_tenant_deployment_controller.rb index 7e57b54..c81e509 100644 --- a/app/controllers/atomic_admin/atomic_tenant_deployment_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_deployment_controller.rb @@ -1,5 +1,5 @@ -module AtomicAdmin - class AtomicTenantDeploymentController < ApplicationController +module AtomicAdmin::Api::Admin::V0 + class AtomicTenantDeploymentController < AdminController def deployment_params params.permit(:iss, :deployment_id, :application_instance_id) end diff --git a/app/controllers/atomic_admin/atomic_tenant_platform_guid_strategy_controller.rb b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_platform_guid_strategy_controller.rb similarity index 94% rename from app/controllers/atomic_admin/atomic_tenant_platform_guid_strategy_controller.rb rename to app/controllers/atomic_admin/api/admin/v0/atomic_tenant_platform_guid_strategy_controller.rb index 0af970f..19b49e7 100644 --- a/app/controllers/atomic_admin/atomic_tenant_platform_guid_strategy_controller.rb +++ b/app/controllers/atomic_admin/api/admin/v0/atomic_tenant_platform_guid_strategy_controller.rb @@ -1,5 +1,5 @@ -module AtomicAdmin - class AtomicTenantPlatformGuidStrategyController < ApplicationController +module AtomicAdmin::Api::Admin::V0 + class AtomicTenantPlatformGuidStrategyController < AdminController if AtomicAdmin.platform_guid_strategy_before_action.present? before_action AtomicAdmin.platform_guid_strategy_before_action, only: [:create, :update] end diff --git a/app/controllers/atomic_admin/api/admin/v0/authenticating_application_controller.rb b/app/controllers/atomic_admin/api/admin/v0/authenticating_application_controller.rb new file mode 100644 index 0000000..5baf99e --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v0/authenticating_application_controller.rb @@ -0,0 +1,6 @@ +module AtomicAdmin::Api::Admin::V0 + class AuthenticatingApplicationController < ActionController::API + include RequireJwtToken + before_action :validate_internal_token + end +end diff --git a/app/controllers/atomic_admin/api/admin/v1/application_instances_controller.rb b/app/controllers/atomic_admin/api/admin/v1/application_instances_controller.rb new file mode 100644 index 0000000..79a8669 --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/application_instances_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + ApplicationInstancesController = AtomicAdmin::V1::ApplicationInstancesController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/applications_controller.rb b/app/controllers/atomic_admin/api/admin/v1/applications_controller.rb new file mode 100644 index 0000000..3df539d --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/applications_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + ApplicationsController = AtomicAdmin::V1::ApplicationsController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/lti_installs_controller.rb b/app/controllers/atomic_admin/api/admin/v1/lti_installs_controller.rb new file mode 100644 index 0000000..767dfcb --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/lti_installs_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + LtiInstallsController = AtomicAdmin::V1::LtiInstallsController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/lti_platforms_controller.rb b/app/controllers/atomic_admin/api/admin/v1/lti_platforms_controller.rb new file mode 100644 index 0000000..0e59276 --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/lti_platforms_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + LtiPlatformsController = AtomicAdmin::V1::LtiPlatformsController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/sites_controller.rb b/app/controllers/atomic_admin/api/admin/v1/sites_controller.rb new file mode 100644 index 0000000..42c0c69 --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/sites_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + SitesController = AtomicAdmin::V1::SitesController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/tenant_client_id_strategies_controller.rb b/app/controllers/atomic_admin/api/admin/v1/tenant_client_id_strategies_controller.rb new file mode 100644 index 0000000..8bd9b3e --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/tenant_client_id_strategies_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + TenantClientIdStrategiesController = AtomicAdmin::V1::TenantClientIdStrategiesController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/tenant_deployments_controller.rb b/app/controllers/atomic_admin/api/admin/v1/tenant_deployments_controller.rb new file mode 100644 index 0000000..7661a03 --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/tenant_deployments_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + TenantDeploymentsController = AtomicAdmin::V1::TenantDeploymentsController +end diff --git a/app/controllers/atomic_admin/api/admin/v1/tenant_platform_guid_strategies_controller.rb b/app/controllers/atomic_admin/api/admin/v1/tenant_platform_guid_strategies_controller.rb new file mode 100644 index 0000000..3cf15fc --- /dev/null +++ b/app/controllers/atomic_admin/api/admin/v1/tenant_platform_guid_strategies_controller.rb @@ -0,0 +1,3 @@ +module AtomicAdmin::Api::Admin::V1 + TenantPlatformGuidStrategiesController = AtomicAdmin::V1::TenantPlatformGuidStrategiesController +end diff --git a/app/controllers/atomic_admin/authenticating_application_controller.rb b/app/controllers/atomic_admin/authenticating_application_controller.rb deleted file mode 100644 index e2a9f38..0000000 --- a/app/controllers/atomic_admin/authenticating_application_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -module AtomicAdmin - class AuthenticatingApplicationController < ActionController::API - include AtomicAdmin::JwtToken - # before_action :authenticate_user! # Use validate_token instead for now - before_action :validate_token - before_action :only_admins! - - private - - def only_admins! - user_not_authorized unless current_user.admin? - end - - def user_not_authorized(message = "Not Authorized") - render json: { message: message, }, status: 401 - end - end -end diff --git a/app/controllers/atomic_admin/v1/admin_controller.rb b/app/controllers/atomic_admin/v1/admin_controller.rb new file mode 100644 index 0000000..1a77f6a --- /dev/null +++ b/app/controllers/atomic_admin/v1/admin_controller.rb @@ -0,0 +1,42 @@ +module AtomicAdmin::V1 + class AdminController < ActionController::API + include RequireJwtToken + before_action :validate_admin_token + + rescue_from ActiveRecord::RecordNotFound, with: :record_not_found + def record_not_found + render_error(:not_found) + end + + protected + + def json_for(resource) + resource.as_json + end + + def json_for_collection(collection) + collection.map { |resource| json_for(resource) } + end + + private + + def render_error(type, message: nil) + case type + when :not_found + [404, { type: "not_found", message: "Record not found" }] + else + [500, { type: "unknown", message: "An error occurred" }] + end => [status, error] + + if message.present? + error[:message] = message + end + + render json: error, status: status + end + + def user_not_authorized(message = "Not Authorized") + render json: { message: message, }, status: 401 + end + end +end diff --git a/app/controllers/atomic_admin/v1/application_instances_controller.rb b/app/controllers/atomic_admin/v1/application_instances_controller.rb new file mode 100644 index 0000000..5bd1958 --- /dev/null +++ b/app/controllers/atomic_admin/v1/application_instances_controller.rb @@ -0,0 +1,123 @@ +module AtomicAdmin::V1 + class ApplicationInstancesController < AdminController + include Filtering + + allowed_sort_columns %w[nickname] + allowed_search_columns %w[nickname] + + def index + @application_instances = ApplicationInstance.where(application_id: params[:application_id]) + @application_instances = + if type == "paid" + @application_instances.where.not(paid_at: nil) + else + @application_instances.where(paid_at: nil) + end + + @application_instances, meta = filter(@application_instances) + + render json: { + application_instances: json_for_collection(@application_instances), + meta: + } + end + + def show + @application_instance = ApplicationInstance.find(params[:id]) + render json: { application_instance: json_for(@application_instance) } + end + + def create + application = Application.find(params[:application_id]) + instance = application.application_instances.new(create_params) + + if instance.save + render json: { application_instance: json_for(instance) } + else + render json: { errors: instance.errors }, status: 422 + end + end + + def update + instance = ApplicationInstance.find(params[:id]) + instance.update(update_params) + + if params[:application_instance][:is_paid] && instance.paid_at.nil? + instance.paid_at = DateTime.now + elsif params[:application_instance][:is_paid] == false && instance.paid_at.present? + instance.paid_at = nil + end + + if instance.save + render json: { application_instance: json_for(instance) } + else + render json: { errors: instance.errors }, status: 422 + end + end + + def destroy + instance = ApplicationInstance.find(params[:id]) + instance.destroy + render json: { success: true } + end + + def interactions + instance = ApplicationInstance.find(params[:id]) + interactions = AtomicAdmin.application_instance_interactions.resolve(application_instance: instance) + render json: { interactions: interactions } + end + + def json_for(instance) + json = instance.as_json(include: [:application, :site]) + + json["trial_start_date"] = instance.trial_start_date&.strftime("%Y-%m-%d") if instance.respond_to?(:trial_start_date) + json["trial_end_date"] = instance.trial_end_date&.strftime("%Y-%m-%d") if instance.respond_to?(:trial_end_date) + json["license_start_date"] = instance.license_start_date&.strftime("%Y-%m-%d") if instance.respond_to?(:license_start_date) + json["license_end_date"] = instance.license_end_date&.strftime("%Y-%m-%d") if instance.respond_to?(:license_end_date) + json["is_paid"] = instance.paid_at.present? if instance.respond_to?(:paid_at) + json["lti_config_xml"] = instance.lti_config_xml if instance.respond_to?(:lti_config_xml) + + json + end + + protected + + def sortable_columns + [ + "created_at", + "trial_end_date", + "trial_users", + "license_end_date", + "licensed_users", + "nickname", + ] + end + + def sort_column + sortable_columns.include?(params[:sort_on]) ? params[:sort_on] : "created_at" + end + + def sort_direction + { + "ascending" => "asc", + "descending" => "desc", + }.fetch(params[:sort_direction], "desc") + end + + def type + params[:type] == "paid" ? "paid" : "evals" + end + + def search + params[:search] + end + + def create_params + params.require(:application_instance).except(:is_paid, :lti_config_xml, :site, :application).permit! + end + + def update_params + create_params + end + end +end diff --git a/app/controllers/atomic_admin/v1/applications_controller.rb b/app/controllers/atomic_admin/v1/applications_controller.rb new file mode 100644 index 0000000..1dd8197 --- /dev/null +++ b/app/controllers/atomic_admin/v1/applications_controller.rb @@ -0,0 +1,49 @@ +module AtomicAdmin::V1 + class ApplicationsController < AdminController + include Filtering + + allowed_sort_columns %w[name] + allowed_search_columns %w[name] + + def index + @applications, meta = filter(Application.all.lti) + render json: { applications: json_for_collection(@applications), meta: } + end + + def show + @application = Application.find(params[:id]) + render json: { application: json_for(@application) } + end + + def update + @application = Application.find(params[:id]) + + # Strong params doesn't allow abritrary json, so we need to set the values manually + @application.default_config = params[:default_config] + @application.canvas_api_permissions = params[:canvas_api_permissions] + + @application.update!(update_params) + render json: { application: json_for(@application) } + end + + def interactions + application = Application.find(params[:id]) + interactions = AtomicAdmin.application_interactions.resolve(application: application) + render json: { interactions: interactions } + end + + def json_for(application) + json = application.as_json.with_indifferent_access + secret = json[:oauth_secret] + json[:oauth_secret_preview] = secret[0..2] + '*' * (secret.length - 3) if secret + + json + end + + protected + + def update_params + params.require(:application).permit! + end + end +end diff --git a/app/controllers/atomic_admin/v1/lti_installs_controller.rb b/app/controllers/atomic_admin/v1/lti_installs_controller.rb new file mode 100644 index 0000000..78840d9 --- /dev/null +++ b/app/controllers/atomic_admin/v1/lti_installs_controller.rb @@ -0,0 +1,42 @@ +module AtomicAdmin::V1 + class LtiInstallsController < AdminController + def index + render json: AtomicLti::Install.all.order(:id).paginate(page: params[:page], per_page: 30) + end + + def create + AtomicLti::Install.create!(create_params) + end + + def show + install = find_install + render json: install + end + + def update + install = find_install + result = install.update!(update_params) + render json: result + end + + def destroy + install = find_install + install.destroy + render json: install + end + + protected + + def find_install + AtomicLti::Install.find_by(id: params[:id]) + end + + def create_params + params.require(:install).permit! + end + + def update_params + params.require(:install).permit! + end + end +end diff --git a/app/controllers/atomic_admin/v1/lti_platforms_controller.rb b/app/controllers/atomic_admin/v1/lti_platforms_controller.rb new file mode 100644 index 0000000..8ef423d --- /dev/null +++ b/app/controllers/atomic_admin/v1/lti_platforms_controller.rb @@ -0,0 +1,50 @@ +module AtomicAdmin::V1 + class LtiPlatformsController < AdminController + include Filtering + + allowed_search_columns %w[iss] + allowed_sort_columns %w[iss] + + def index + platforms, meta = filter(AtomicLti::Platform.all) + + render json: { platforms:, meta: } + end + + def create + platform = AtomicLti::Platform.create!(create_params) + render json: { platform: platform } + end + + def show + platform = find_platform + render json: platform + end + + def update + platform = find_platform + platform.update!(update_params) + render json: { platform: find_platform } + end + + def destroy + platform = find_platform + platform.destroy + render json: platform + end + + protected + + def create_params + params.require(:platform).permit! + end + + def update_params + params.require(:platform).permit! + end + + def find_platform + AtomicLti::Platform.find_by(id: params[:id]) + end + end +end diff --git a/app/controllers/atomic_admin/v1/sites_controller.rb b/app/controllers/atomic_admin/v1/sites_controller.rb new file mode 100644 index 0000000..07f810f --- /dev/null +++ b/app/controllers/atomic_admin/v1/sites_controller.rb @@ -0,0 +1,45 @@ +module AtomicAdmin::V1 + class SitesController < AdminController + include Filtering + + allowed_search_columns %w[url] + allowed_sort_columns %w[url] + + def index + @sites = Site.all + sites, meta = filter(@sites) + render json: { sites: json_for_collection(sites), meta: } + end + + def create + @site = Site.create!(create_params) + render json: { site: json_for(@site) } + end + + def update + @site = Site.find(params[:id]) + @site.update!(update_params) + render json: { site: json_for(@site) } + end + + def destroy + @site = Site.find(params[:id]) + @site.destroy! + render json: { site: json_for(@site) } + end + + protected + + def json_for(site) + site.as_json + end + + def create_params + params.require(:site).permit! + end + + def update_params + params.require(:site).permit! + end + end +end diff --git a/app/controllers/atomic_admin/v1/tenant_client_id_strategies_controller.rb b/app/controllers/atomic_admin/v1/tenant_client_id_strategies_controller.rb new file mode 100644 index 0000000..77d905e --- /dev/null +++ b/app/controllers/atomic_admin/v1/tenant_client_id_strategies_controller.rb @@ -0,0 +1,48 @@ +module AtomicAdmin::V1 + class TenantClientIdStrategiesController < AdminController + include Filtering + + allowed_search_columns %w[client_id, iss] + allowed_sort_columns %w[client_id, iss] + + def index + query = AtomicTenant::PinnedClientId.where(application_instance_id:) + page, meta = filter(query) + + render json: { + pinned_client_ids: page, + meta: + } + end + + def show + pinned_client_id = find_pinned_client_id + render json: {pinned_client_id: pinned_client_id} + end + + def create + result = AtomicTenant::PinnedClientId.create!({**create_params, application_instance_id:}) + render json: { pinned_client_id: result } + end + + def destroy + pinned_client_id = find_pinned_client_id + pinned_client_id.destroy + render json: { pinned_client_id: pinned_client_id } + end + + protected + + def application_instance_id + params[:application_instance_id] || params[:application_instance_id] + end + + def create_params + params.require(:pinned_client_id).permit! + end + + def find_pinned_client_id + AtomicTenant::PinnedClientId.find_by(id: params[:id]) + end + end +end diff --git a/app/controllers/atomic_admin/v1/tenant_deployments_controller.rb b/app/controllers/atomic_admin/v1/tenant_deployments_controller.rb new file mode 100644 index 0000000..22f17f5 --- /dev/null +++ b/app/controllers/atomic_admin/v1/tenant_deployments_controller.rb @@ -0,0 +1,47 @@ +module AtomicAdmin::V1 + class TenantDeploymentsController < AdminController + include Filtering + + allowed_search_columns %w[deployment_id, iss] + allowed_sort_columns %w[deployment_id, iss] + + def index + page, meta = filter(AtomicTenant::LtiDeployment.where(application_instance_id:)) + + render json: { + deployments: page, + meta: + } + end + + def create + result = AtomicTenant::LtiDeployment.create!({**create_params, application_instance_id:}) + render json: { deployment: result } + end + + def show + deployment = find_deployment + render json: { deployment: deployment } + end + + def destroy + deployment = find_deployment + deployment.destroy + render json: { deployment: deployment } + end + + protected + + def application_instance_id + params[:application_instance_id] + end + + def create_params + params.require(:deployment).permit! + end + + def find_deployment + AtomicTenant::LtiDeployment.find_by(id: params[:id]) + end + end +end diff --git a/app/controllers/atomic_admin/v1/tenant_platform_guid_strategies_controller.rb b/app/controllers/atomic_admin/v1/tenant_platform_guid_strategies_controller.rb new file mode 100644 index 0000000..cbe0b97 --- /dev/null +++ b/app/controllers/atomic_admin/v1/tenant_platform_guid_strategies_controller.rb @@ -0,0 +1,63 @@ +module AtomicAdmin::V1 + class TenantPlatformGuidStrategiesController < AdminController + include Filtering + + allowed_search_columns %w[platform_guid, iss] + allowed_sort_columns %w[platform_guid, iss] + + def index + query = AtomicTenant::PinnedPlatformGuid.where(application_instance_id:) + page, meta = filter(query) + + render json: { + pinned_platform_guids: page, + meta: + } + end + + def create + result = AtomicTenant::PinnedPlatformGuid.create!({**create_params, application_instance_id:, application_id:}) + render json: { pinned_platform_guid: result } + end + + def show + pinned_platform_guid = find_pinned_platform_guid + render json: {pinned_platform_guid: pinned_platform_guid} + end + + def update + pinned_platform_guid = find_pinned_platform_guid + pinned_platform_guid.update!(update_params) + + render json: {pinned_platform_guid: find_pinned_platform_guid} + end + + def destroy + pinned_platform_guid = find_pinned_platform_guid + pinned_platform_guid.destroy + render json: { pinned_platform_guid: pinned_platform_guid } + end + + protected + + def application_id + params[:application_id] + end + + def application_instance_id + params[:application_instance_id] + end + + def create_params + params.require(:pinned_platform_guid).permit! + end + + def update_params + params.require(:pinned_platform_guid).permit! + end + + def find_pinned_platform_guid + AtomicTenant::PinnedPlatformGuid.find(params[:id]) + end + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/concerns/filtering.rb b/app/controllers/concerns/filtering.rb new file mode 100644 index 0000000..f396af4 --- /dev/null +++ b/app/controllers/concerns/filtering.rb @@ -0,0 +1,54 @@ +module Filtering + extend ActiveSupport::Concern + + included do + class_variable_set(:@@allowed_sort_columns, []) + class_variable_set(:@@allowed_search_columns, []) + end + + class_methods do + def allowed_sort_columns(names) + class_variable_set(:@@allowed_sort_columns, names) + end + + def allowed_search_columns(names) + class_variable_set(:@@allowed_search_columns, names) + end + end + + def query_params + params. + permit(:search, :sort_on, :sort_direction, :page, :per_page, :search_on). + with_defaults(sort_direction: "asc", page: 1, per_page: 30) + end + + def filter(relation) + params = query_params + allowed_search_columns = self.class.class_variable_get(:@@allowed_search_columns) + + if params[:search].present? && params[:search_on].present? && allowed_search_columns.include?(params[:search_on]) + relation = relation.where("lower(#{params[:search_on]}) LIKE ?", "%#{params[:search].downcase}%") + end + + allowed_sort_columns = self.class.class_variable_get(:@@allowed_sort_columns) + if params[:sort_on].present? && allowed_sort_columns.include?(params[:sort_on]) + sort_col = params[:sort_on] + sort_dir = params[:sort_direction] + sort_dir = "asc" if sort_dir == "ascending" + sort_dir = "desc" if sort_dir == "descending" + relation = relation.order({sort_col => sort_dir}) + end + + relation = relation.paginate(page: params[:page], per_page: params[:per_page]) + + meta = { + current_page: relation.current_page, + next_page: relation.next_page, + prev_page: relation.previous_page, + total_pages: relation.total_pages, + total_items: relation.total_entries, + } + + [relation, meta] + end +end diff --git a/app/controllers/concerns/require_jwt_token.rb b/app/controllers/concerns/require_jwt_token.rb new file mode 100644 index 0000000..00c80b0 --- /dev/null +++ b/app/controllers/concerns/require_jwt_token.rb @@ -0,0 +1,57 @@ +module RequireJwtToken + extend ActiveSupport::Concern + + protected + + def validate_admin_token + encoded_token = get_encoded_token(request) + decoder = AtomicAdmin::JwtToken::JwksDecoder.new(AtomicAdmin.admin_jwks_url) + token = decoder.decode(encoded_token)&.first + validate_claims!(token) + token + + rescue JWT::DecodeError, AtomicAdmin::JwtToken::InvalidTokenError => e + Rails.logger.error "JWT Error occured #{e.inspect}" + render json: { error: "Unauthorized: Invalid token." }, status: :unauthorized + end + + def validate_internal_token + encoded_token = get_encoded_token(request) + decoder = AtomicAdmin::JwtToken::SecretDecoder.new(AtomicAdmin.internal_secret) + token = decoder.decode!(encoded_token) + validate_claims!(token) + + current_application_instance_id = request.env['atomic.validated.application_instance_id'] + if current_application_instance_id && current_application_instance_id != token["application_instance_id"] + raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid application instance id" + end + + @user_tenant = token["user_tenant"] if token["user_tenant"].present? + @user = User.find(token["user_id"]) + + sign_in(@user, event: :authentication, store: false) + rescue JWT::DecodeError, AtomicAdmin::JwtToken::InvalidTokenError => e + Rails.logger.error "Internal JWT Error occured #{e.inspect}" + render json: { error: "Unauthorized: Invalid token." }, status: :unauthorized + end + + private + + def get_encoded_token(req) + return req.params[:jwt] if req.params[:jwt] + + header = req.headers["Authorization"] || req.headers[:authorization] + raise AtomicAdmin::JwtToken::MissingTokenError, "No authorization header found" if header.nil? + + token = header.split(" ").last + raise AtomicAdmin::JwtToken::MissingTokenError, "Invalid authorization header string" if token.nil? + + token + end + + def validate_claims!(token) + if AtomicAdmin.audience != token["aud"] + raise AtomicAdmin::JwtToken::InvalidTokenError, "Expected audience to be #{AtomicAdmin.audience} but was #{token["aud"]}" + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 47f842e..80764cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,13 +1,44 @@ AtomicAdmin::Engine.routes.draw do - # namespace :lti do - resources :atomic_lti_platform - resources :atomic_lti_install - resources :atomic_tenant_deployment - post '/atomic_tenant_deployment/search', to: 'atomic_tenant_deployment#search' + namespace :api do + namespace :admin do + # NOTE: these are the "legacy" routes that the old admin app relies on. They don't follow the same conventions as the new API. + # They are also not namespaces under /api/admin/v0 but rather /api/admin/* + scope module: "v0" do + resources :atomic_lti_platform + resources :atomic_lti_install + resources :atomic_tenant_deployment + post '/atomic_tenant_deployment/search', to: 'atomic_tenant_deployment#search' - resources :atomic_tenant_platform_guid_strategy - post '/atomic_tenant_platform_guid_strategy/search', to: 'atomic_tenant_platform_guid_strategy#search' - post '/atomic_tenant_client_id_strategy/search', to: 'atomic_tenant_client_id_strategy#search' + resources :atomic_tenant_platform_guid_strategy + post '/atomic_tenant_platform_guid_strategy/search', to: 'atomic_tenant_platform_guid_strategy#search' - resources :atomic_tenant_client_id_strategy + resources :atomic_tenant_client_id_strategy + post '/atomic_tenant_client_id_strategy/search', to: 'atomic_tenant_client_id_strategy#search' + end + + namespace :v1 do + resources :lti_platforms + resources :lti_installs + resources :tenant_deployments + resources :sites + + resources :applications do + member do + get :interactions + end + + resources :application_instances do + member do + get :interactions + end + + resources :tenant_client_id_strategies + resources :tenant_platform_guid_strategies + resources :tenant_deployments + resources :stats + end + end + end + end + end end diff --git a/docs/interactions.md b/docs/interactions.md new file mode 100644 index 0000000..23072ce --- /dev/null +++ b/docs/interactions.md @@ -0,0 +1,119 @@ +# Interactions + +## `analytics` + +Display a custom analytics dashboard. + +```ruby +config.application_instance_interactions.tap do |inter| + inter.add( + :analytics, + type: :analytics, + title: "Analytics", + icon: "bar_chart", + ) +end +``` + +## `jsonform` + +Display a custom JSON form using [JSONForms](https://jsonforms.io/). + +```ruby +config.application_instance_interactions.tap do |inter| + inter.add( + :general_settings, + type: :jsonform, + title: "General Settings", + icon: "settings", + schema: AtomicAdmin::Schema::ApplicationInstanceGeneralSettingsSchema, + ) +end +``` + +### Pre-defined Schemas + +AtomicAdmin provides several pre-defined schemas that can be used with the `jsonform` interaction: + +- **ApplicationInstanceGeneralSettingsSchema**: General settings for an application instance, including nickname, primary contact, LMS URL, domain settings, and LTI configurations. + +- **ApplicationInstanceLicenseDetailsSchema**: Manage license details including paid status, license dates, number of licensed users, and license type (monthly, yearly, or FTE). + +- **ApplicationInstanceTrialDetailsSchema**: Configure trial details including start/end dates, number of trial users, and trial notes. + +- **ApplicationInstanceXmlConfigSchema**: Manage LTI 1.1 configurations, including key, secret, and XML configuration. + +- **ApplicationInstanceConfigurationSchema**: Edit custom application instance configuration and LTI configuration as JSON. + +- **AtomicApplicationUpdateSchema**: Update application settings including description, OAuth key, and OAuth secret. + +- **ApplicationInstanceCreateSchema**: Create a new application instance with basic settings like nickname, primary contact, LTI key, and site. + +### Creating Custom Schemas + +You can create your own schema by defining two methods: `schema` and `uischema`. The `schema` method defines the JSON schema, while the `uischema` method defines the UI schema. + +```ruby +class YourCustomSchema + def schema + # Define your JSON Schema here + { + type: "object", + properties: { + field_one: { + type: "string", + minLength: 1, + }, + field_two: { + type: ["string", "null"], + }, + # Add more fields as needed + }, + required: ["field_one"], + } + end + + def uischema + # Define your UI Schema here (layout) + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/field_one", + options: { + # Optional formatting options + format: "textarea", + }, + }, + { + type: "Control", + scope: "#/properties/field_two", + }, + ] + } + end +end +``` + +For more complex schemas, you can: + +- Use nested layouts with `Group`, `VerticalLayout`, and `HorizontalLayout` +- Add custom formatting with the `options` property +- Define field validation with JSON schema properties like `minLength`, `pattern`, etc. +- Use `oneOf` with mapped values for dropdowns and radio buttons + +## `lti_advantage` + +Displays a page for managing pinned Client Ids, Deployments, and Platform Instance GUIDS + +```ruby +config.application_instance_interactions.tap do |inter| + inter.add( + :lti_advantage, + type: :lti_advantage, + title: "LTI Advantage", + icon: "settings", + ) +end +``` diff --git a/docs/overriding-controllers.md b/docs/overriding-controllers.md new file mode 100644 index 0000000..852ee35 --- /dev/null +++ b/docs/overriding-controllers.md @@ -0,0 +1,20 @@ +# Overriding Controllers + +The controllers that the gem provides are designed to be overridable. + +To override the `ApplicationInstancesController` you create a new controller at the correct path, and can inherit from the gem's controller. For example: + +```ruby +# app/controllers/atomic_admin/api/admin/v1/application_instances_controller.rb + +class AtomicAdmin::Api::Admin::V1::ApplicationInstancesController < AtomicAdmin::V1::ApplicationInstancesController + # Override the index action + def index + # Custom logic here + super # Call the original method if needed + end + + # Add any other custom actions or overrides here +end +``` + diff --git a/lib/atomic_admin.rb b/lib/atomic_admin.rb index 793a649..b72e4c6 100644 --- a/lib/atomic_admin.rb +++ b/lib/atomic_admin.rb @@ -1,13 +1,20 @@ require "atomic_admin/version" require "atomic_admin/engine" require "atomic_admin/jwt_token" +require "atomic_admin/schema" +require "atomic_admin/interaction" module AtomicAdmin - # Base controller class to inherit from. If this is set it is responsible for - # all authentication + mattr_accessor :admin_jwks_url + mattr_accessor :audience + mattr_accessor :internal_secret + mattr_accessor :application_interactions, default: AtomicAdmin::Interaction::Manager.new + mattr_accessor :application_instance_interactions, default: AtomicAdmin::Interaction::Manager.new mattr_accessor :authenticating_base_controller_class, default: nil - - # Before action hooks to allow custom validation mattr_accessor :client_id_strategy_before_action, default: nil mattr_accessor :platform_guid_strategy_before_action, default: nil + + def self.configure + yield self + end end diff --git a/lib/atomic_admin/interaction.rb b/lib/atomic_admin/interaction.rb new file mode 100644 index 0000000..1ad1411 --- /dev/null +++ b/lib/atomic_admin/interaction.rb @@ -0,0 +1,58 @@ +module AtomicAdmin::Interaction + class Manager + def initialize + @interactions = {} + @curr_index = 0 + end + + def add(key, **kwargs) + @interactions[key] = { + **kwargs, + order: @curr_index, + } + + if @interactions[key][:type] == :analytics && @interactions[key][:controller].present? + controller_class = @interactions[key][:controller] + Rails.application.config.to_prepare do + AtomicAdmin::Api::Admin::V1.const_set(:StatsController, controller_class.constantize) + end + end + @curr_index += 1 + end + + def get(key) + @interactions[key] + end + + def tap + yield self + self + end + + def resolve(**kwargs) + sorted = @interactions.sort_by { |key, interaction| interaction[:order] } + sorted.map do |key, interaction| + type = interaction[:type] + hash = { + key: key, + type: type, + title: interaction[:title], + icon: interaction[:icon], + } + + case type + when :jsonform + schema_factory = interaction[:schema] + schema = schema_factory.new(**kwargs) + hash[:schema] = schema.schema + hash[:uischema] = schema.uischema + when :launch + hash[:launch_url] = interaction[:launch_url].call(**kwargs) + hash[:aud] = interaction[:aud] + end + + hash + end + end + end +end diff --git a/lib/atomic_admin/jwt_token.rb b/lib/atomic_admin/jwt_token.rb index d514052..00d972d 100644 --- a/lib/atomic_admin/jwt_token.rb +++ b/lib/atomic_admin/jwt_token.rb @@ -1,69 +1,9 @@ -## Note: This code is basically copied out of the starter app to authenticate -## admin app api calls. Lives at /app/controllers/concerns/jwt_token.rb in -## starter app +require_relative 'jwt_token/jwks_decoder' +require_relative 'jwt_token/secret_decoder' + module AtomicAdmin module JwtToken - - ALGORITHM = "HS512".freeze - class InvalidTokenError < StandardError; end - - def self.valid?(token, secret = nil, algorithm = ALGORITHM) - decode(token, secret, true, algorithm) - end - - def self.decode(token, secret = nil, validate = true, algorithm = ALGORITHM) - JWT.decode( - token, - secret || Rails.application.secrets.auth0_client_secret, - validate, - { algorithm: algorithm }, - ) - end - - def decoded_jwt_token(req, secret = nil) - token = AtomicAdmin::JwtToken.valid?(encoded_token(req), secret) - raise InvalidTokenError, "Unable to decode jwt token" if token.blank? - raise InvalidTokenError, "Invalid token payload" if token.empty? - - token[0] - end - - def validate_token - token = decoded_jwt_token(request) - raise InvalidTokenError if Rails.application.secrets.auth0_client_id != token["aud"] - - current_application_instance_id = request.env['atomic.validated.application_instance_id'] - if current_application_instance_id && current_application_instance_id != token["application_instance_id"] - raise InvalidTokenError - end - - @user_tenant = token["user_tenant"] if token["user_tenant"].present? - @user = User.find(token["user_id"]) - - sign_in(@user, event: :authentication, store: false) - rescue JWT::DecodeError, InvalidTokenError => e - Rails.logger.error "JWT Error occured #{e.inspect}" - begin - render json: { error: "Unauthorized: Invalid token." }, status: :unauthorized - rescue NoMethodError - raise GraphQL::ExecutionError, "Unauthorized: Invalid token." - end - end - - protected - - def encoded_token(req) - return req.params[:jwt] if req.params[:jwt] - - header = req.headers["Authorization"] || req.headers[:authorization] - raise InvalidTokenError, "No authorization header found" if header.nil? - - token = header.split(" ").last - raise InvalidTokenError, "Invalid authorization header string" if token.nil? - - token - end - + class MissingTokenError < StandardError; end end -end \ No newline at end of file +end diff --git a/lib/atomic_admin/jwt_token/jwks_decoder.rb b/lib/atomic_admin/jwt_token/jwks_decoder.rb new file mode 100644 index 0000000..0dd2cfa --- /dev/null +++ b/lib/atomic_admin/jwt_token/jwks_decoder.rb @@ -0,0 +1,41 @@ +module AtomicAdmin::JwtToken + # Decodes a JWT token using the JWKS endpoint + # This is used for decoding JWT tokens issued by the new + # admin app + class JwksDecoder + ALGORITHMS = ["RS256"].freeze + + def initialize(jwks_url, algorithms = ALGORITHMS) + @jwks_url = jwks_url + @algorithms = algorithms + end + + def decode(token, validate = true) + load_admin_jwks = ->(options) do + Rails.cache.delete("atomic_admin_jwks") if options[:kid_not_found] + + # NOTE: the cached keys only expire when we recieve a kid_not_found error + keys = Rails.cache.fetch("atomic_admin_jwks") do + HTTParty.get(@jwks_url).parsed_response + end + + JWT::JWK::Set.new(keys).select { |k| k[:use] == "sig" } + end + + JWT.decode( + token, + nil, + validate, + { algorithms: @algorithms, jwks: load_admin_jwks }, + ) + end + + def decode!(token) + token = decode(token) + raise AtomicAdmin::JwtToken::InvalidTokenError, "Unable to decode jwt token" if token.blank? + raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid token payload" if token.empty? + + token[0] + end + end +end diff --git a/lib/atomic_admin/jwt_token/secret_decoder.rb b/lib/atomic_admin/jwt_token/secret_decoder.rb new file mode 100644 index 0000000..00b9214 --- /dev/null +++ b/lib/atomic_admin/jwt_token/secret_decoder.rb @@ -0,0 +1,29 @@ +module AtomicAdmin::JwtToken + # Decodes a JWT token using a known secret. This is used for decoding + # JWT tokens issued by the application itself for the old admin app + class SecretDecoder + ALGORITHM = "HS512".freeze + + def initialize(secret, algorithm = ALGORITHM) + @secret = secret + @algorithm = algorithm + end + + def decode(token, validate = true) + JWT.decode( + token, + @secret, + validate, + { algorithm: @algorithm }, + ) + end + + def decode!(token) + token = decode(token) + raise AtomicAdmin::JwtToken::InvalidTokenError, "Unable to decode jwt token" if token.blank? + raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid token payload" if token.empty? + + token[0] + end + end +end diff --git a/lib/atomic_admin/schema.rb b/lib/atomic_admin/schema.rb new file mode 100644 index 0000000..7f5b727 --- /dev/null +++ b/lib/atomic_admin/schema.rb @@ -0,0 +1,13 @@ +require_relative "schema/atomic_application_update_schema" +require_relative "schema/application_instance_create_schema" +require_relative "schema/application_instance_schema" +require_relative "schema/application_instance_configuration_schema" +require_relative "schema/application_instance_general_settings_schema" +require_relative "schema/application_instance_xml_config_schema" +require_relative "schema/application_instance_trial_details_schema" +require_relative "schema/application_instance_license_details_schema" + +module AtomicAdmin + module Schema + end +end diff --git a/lib/atomic_admin/schema/application_instance_configuration_schema.rb b/lib/atomic_admin/schema/application_instance_configuration_schema.rb new file mode 100644 index 0000000..6b3eef3 --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_configuration_schema.rb @@ -0,0 +1,61 @@ +module AtomicAdmin::Schema + class ApplicationInstanceConfigurationSchema < ApplicationInstanceSchema + def schema + { + type: "object", + properties: { + config: { + type: "object", + }, + lti_config: { + type: "object", + }, + }, + } + end + + def uischema + { + type: "VerticalLayout", + elements: [ + { + type: "Group", + label: "Custom App Instance Configuration", + elements: [ + { + type: "Control", + scope: "#/properties/config", + options: { + format: "json", + props: { + size: "full", + label: "", + height: "400px", + }, + }, + }, + ], + }, + { + type: "Group", + label: "LTI Config", + elements: [ + { + type: "Control", + scope: "#/properties/lti_config", + options: { + format: "json", + props: { + size: "full", + label: "", + height: "400px", + }, + }, + }, + ], + }, + ], + } + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_create_schema.rb b/lib/atomic_admin/schema/application_instance_create_schema.rb new file mode 100644 index 0000000..41e2afb --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_create_schema.rb @@ -0,0 +1,77 @@ + +module AtomicAdmin::Schema + class ApplicationInstanceCreateSchema + attr_accessor :application + + def initialize(application) + @application = application + end + + def schema + sites = Site.all + + { + type: "object", + required: ["nickname", "site_id", "lti_key"], + properties: { + nickname: { + type: "string", + minLength: 1, + }, + primary_contact: { + type: ["string", "null"], + }, + lti_key: { + type: "string", + }, + site_id: { + type: "number", + oneOf: sites.map do |site| + { + title: site.url, + const: site.id + } + end + }, + }, + } + end + + def uischema + { + type: "VerticalLayout", + elements: [ + { + type: "HorizontalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/nickname", + }, + { + type: "Control", + scope: "#/properties/primary_contact", + }, + ], + }, + { + type: "HorizontalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/site_id", + props: { + label: "Site" + } + }, + { + type: "Control", + scope: "#/properties/lti_key", + }, + ] + } + ] + } + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_general_settings_schema.rb b/lib/atomic_admin/schema/application_instance_general_settings_schema.rb new file mode 100644 index 0000000..6baa35a --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_general_settings_schema.rb @@ -0,0 +1,156 @@ +module AtomicAdmin::Schema + class ApplicationInstanceGeneralSettingsSchema < ApplicationInstanceSchema + + def schema + sites = Site.all + + { + type: "object", + properties: { + nickname: { + type: "string", + minLength: 1, + }, + primary_contact: { + type: ["string", "null"], + }, + created_at: { + type: "string", + format: "date-time", + }, + lti_key: { + type: "string", + }, + lti_secret: { + type: "string", + }, + # The available sites is based on the sites that have been created + # so this would require it to be dynamically generated on the fly + site_id: { + type: "number", + oneOf: sites.map do |site| + { + title: site.url, + const: site.id + } + end + }, + domain: { + type: "string", + }, + canvas_token: { + type: ["string", "null"], + }, + rollbar_enabled: { + type: "boolean", + }, + use_scoped_developer_key: { + type: "boolean", + }, + }, + required: ["nickname"], + } + end + + def uischema + token_preview = @application_instance.canvas_token_preview() || "Not set" + { + type: "HorizontalLayout", + elements: [ + { + type: "Group", + label: "General Settings", + elements: [ + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/nickname", + }, + { + type: "Control", + scope: "#/properties/primary_contact", + }, + { + type: "Control", + scope: "#/properties/created_at", + label: "Date Created", + options: { + format: "date", + props: { + isReadOnly: true, + size: "small", + }, + }, + }, + { + type: "Control", + scope: "#/properties/site_id", + label: "LMS URL", + options: { + props: { + menuSize: "auto", + } + } + }, + { + type: "Control", + scope: "#/properties/domain", + label: "LTI Tool Domain", + }, + { + type: "Control", + scope: "#/properties/canvas_token", + label: "Canvas Token", + options: { + props: { + message: "Current Canvas Token: #{token_preview}", + }, + }, + }, + { + type: "Control", + scope: "#/properties/rollbar_enabled", + label: "Enable Rollbar", + }, + { + type: "Control", + scope: "#/properties/use_scoped_developer_key", + label: "Use Scoped Developer Key", + }, + ], + }, + ], + }, + { + type: "Group", + label: "LTI Key and Secret", + elements: [ + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/lti_key", + label: "LTI Key", + options: { + props: { + isReadOnly: true, + }, + }, + }, + { + type: "Control", + scope: "#/properties/lti_secret", + label: "LTI Secret", + }, + ], + }, + ], + }, + ], + } + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_license_details_schema.rb b/lib/atomic_admin/schema/application_instance_license_details_schema.rb new file mode 100644 index 0000000..083e7e2 --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_license_details_schema.rb @@ -0,0 +1,99 @@ + +module AtomicAdmin::Schema + class ApplicationInstanceLicenseDetailsSchema < ApplicationInstanceSchema + + def schema + { + type: "object", + properties: { + is_paid: { + type: "boolean", + title: "Paid Account" + }, + license_start_date: { + type: ["string", "null"], + format: "date", + }, + license_end_date: { + type: ["string", "null"], + format: "date", + }, + licensed_users: { + type: ["number", "null"], + minimum: 0, + }, + license_type: { + type: ["string", "null"], + oneOf: [ + { + title: "Monthly", + const: "monthly", + }, + { + title: "Yearly", + const: "yearly", + }, + { + title: "FTE", + const: "fte", + }, + ], + }, + license_notes: { + type: ["string", "null"], + }, + }, + } + end + + def uischema + { + type: "Group", + label: "License Details", + elements: [ + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/is_paid", + }, + { + type: "Control", + scope: "#/properties/license_start_date", + options: { + format: "date", + }, + }, + { + type: "Control", + scope: "#/properties/license_end_date", + options: { + format: "date", + }, + }, + { + type: "Control", + scope: "#/properties/licensed_users", + }, + { + type: "Control", + scope: "#/properties/license_type", + options: { + format: "radio", + }, + }, + { + type: "Control", + scope: "#/properties/license_notes", + options: { + format: "textarea", + }, + }, + ], + }, + ], + } + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_schema.rb b/lib/atomic_admin/schema/application_instance_schema.rb new file mode 100644 index 0000000..a95388a --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_schema.rb @@ -0,0 +1,18 @@ + +module AtomicAdmin::Schema + class ApplicationInstanceSchema + attr_reader :application_instance + + def initialize(application_instance:) + @application_instance = application_instance + end + + def schema + raise "Not implemented" + end + + def uischema + raise "Not implemented" + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_trial_details_schema.rb b/lib/atomic_admin/schema/application_instance_trial_details_schema.rb new file mode 100644 index 0000000..a428195 --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_trial_details_schema.rb @@ -0,0 +1,67 @@ + +module AtomicAdmin::Schema + class ApplicationInstanceTrialDetailsSchema < ApplicationInstanceSchema + + def schema + { + type: "object", + properties: { + trial_start_date: { + type: ["string", "null"], + format: "date", + }, + trial_end_date: { + type: ["string", "null"], + format: "date", + }, + trial_users: { + type: ["number", "null"], + minimum: 0, + }, + trial_notes: { + type: ["string", "null"], + }, + }, + } + end + + def uischema + { + type: "Group", + label: "License Details", + elements: [ + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/trial_start_date", + options: { + format: "date", + }, + }, + { + type: "Control", + scope: "#/properties/trial_end_date", + options: { + format: "date", + }, + }, + { + type: "Control", + scope: "#/properties/trial_users", + }, + { + type: "Control", + scope: "#/properties/trial_notes", + options: { + format: "textarea", + }, + }, + ], + }, + ], + } + end + end +end diff --git a/lib/atomic_admin/schema/application_instance_xml_config_schema.rb b/lib/atomic_admin/schema/application_instance_xml_config_schema.rb new file mode 100644 index 0000000..27f1e37 --- /dev/null +++ b/lib/atomic_admin/schema/application_instance_xml_config_schema.rb @@ -0,0 +1,66 @@ +module AtomicAdmin::Schema + class ApplicationInstanceXmlConfigSchema < ApplicationInstanceSchema + def schema + { + type: "object", + properties: { + lti_key: { + type: "string", + }, + lti_secret: { + type: "string", + }, + lti_config_xml: { + type: "string", + }, + } + } + end + + def uischema + { + type: "Group", + label: "LTI 1.1", + elements: [ + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/lti_key", + options: { + props: { + label: "LTI Key", + isReadOnly: true, + }, + }, + }, + { + type: "Control", + scope: "#/properties/lti_secret", + options: { + props: { + label: "LTI Secret", + }, + }, + }, + { + type: "Control", + scope: "#/properties/lti_config_xml", + options: { + format: "textarea", + props: { + label: "LTI Config XML", + isReadOnly: true, + size: "full", + rows: 20, + }, + }, + }, + ], + }, + ], + } + end + end +end diff --git a/lib/atomic_admin/schema/atomic_application_update_schema.rb b/lib/atomic_admin/schema/atomic_application_update_schema.rb new file mode 100644 index 0000000..7193a6c --- /dev/null +++ b/lib/atomic_admin/schema/atomic_application_update_schema.rb @@ -0,0 +1,57 @@ +module AtomicAdmin::Schema + class AtomicApplicationUpdateSchema + attr_accessor :application + + def initialize(application) + @application = application + end + + def schema + { + type: "object", + properties: { + description: { + type: "string", + }, + oauth_key: { + type: ["string", "null"], + }, + oauth_secret: { + type: ["string", "null"], + secret: { + preview: "ouath_secret_preview", + value: "oauth_secret" + } + }, + } + } + end + + def uischema + { + type: "VerticalLayout", + elements: [ + { + type: "Control", + scope: "#/properties/description", + options: { + format: "textarea", + props: { + size: "full", + resize: "vertical" + } + } + }, + { + type: "Control", + scope: "#/properties/oauth_key", + }, + { + type: "Control", + scope: "#/properties/oauth_secret", + }, + ] + } + end + end +end diff --git a/lib/atomic_admin/version.rb b/lib/atomic_admin/version.rb index 812c451..e6d6b23 100644 --- a/lib/atomic_admin/version.rb +++ b/lib/atomic_admin/version.rb @@ -1,3 +1,3 @@ module AtomicAdmin - VERSION = "1.1.1" + VERSION = "2.0.0.beta.5".freeze end diff --git a/test/controllers/atomic_admin/api/sites_controller_test.rb b/test/controllers/atomic_admin/api/sites_controller_test.rb new file mode 100644 index 0000000..2fb314d --- /dev/null +++ b/test/controllers/atomic_admin/api/sites_controller_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +module AtomicAdmin + class Api::SitesControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + # test "the truth" do + # assert true + # end + end +end diff --git a/test/controllers/atomic_admin/atomic_admin/atomic_application_instances_controller_test.rb b/test/controllers/atomic_admin/atomic_admin/atomic_application_instances_controller_test.rb new file mode 100644 index 0000000..88c913a --- /dev/null +++ b/test/controllers/atomic_admin/atomic_admin/atomic_application_instances_controller_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +module AtomicAdmin + class AtomicAdmin::AtomicApplicationInstancesControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + # test "the truth" do + # assert true + # end + end +end diff --git a/test/controllers/atomic_admin/atomic_admin/atomic_applications_controller_test.rb b/test/controllers/atomic_admin/atomic_admin/atomic_applications_controller_test.rb new file mode 100644 index 0000000..c35046f --- /dev/null +++ b/test/controllers/atomic_admin/atomic_admin/atomic_applications_controller_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +module AtomicAdmin + class AtomicAdmin::AtomicApplicationsControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + # test "the truth" do + # assert true + # end + end +end