diff --git a/app/assets/stylesheets/settings/users.scss b/app/assets/stylesheets/settings/users.scss index 383a4d4ff..d2822724b 100644 --- a/app/assets/stylesheets/settings/users.scss +++ b/app/assets/stylesheets/settings/users.scss @@ -146,3 +146,36 @@ } } } + +.api-key-container { + border: $border-default; + padding: 1em; + + .title { + margin-top: 0; + } + + .description { + margin-bottom: .5em; + } + + .api-key-display { + margin: 1em 0; + } + + .api-key-field { + width: 100%; + } + + .api-key-controls { + display: flex; + + .btn { + margin-right: .5em; + } + } + + .api-key-error { + color: $brand-danger; + } +} diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index f1fdc5d96..72eb1780e 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -106,6 +106,14 @@ module Api ) end + rescue_from Api::V1::ApiKeyError do |e| + render_error( + e.message, + I18n.t('api.core.invalid_api_key_detail'), + :unauthorized + ) + end + before_action :check_include_param, only: %i(index show) def index diff --git a/app/controllers/concerns/token_authentication.rb b/app/controllers/concerns/token_authentication.rb index 46f967bfa..1bcb4aa24 100644 --- a/app/controllers/concerns/token_authentication.rb +++ b/app/controllers/concerns/token_authentication.rb @@ -1,5 +1,12 @@ # frozen_string_literal: true +module Api + module V1 + class ApiKeyError < StandardError + end + end +end + module TokenAuthentication extend ActiveSupport::Concern @@ -13,7 +20,21 @@ module TokenAuthentication raise JWT::InvalidPayload, I18n.t('api.core.no_azure_user_mapping') unless current_user end + def authenticate_with_api_key + @api_key = request.headers['Api-Key'] + return unless @api_key + + @current_user = User.from_api_key(@api_key) + + raise Api::V1::ApiKeyError, I18n.t('api.core.invalid_api_key') unless @current_user + + @current_user + end + def authenticate_request! + # API key authentication successful + return if authenticate_with_api_key + @token = request.headers['Authorization']&.sub('Bearer ', '') raise JWT::VerificationError, I18n.t('api.core.missing_token') unless @token diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 33b948b9b..5758f98dc 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -207,6 +207,18 @@ class Users::RegistrationsController < Devise::RegistrationsController render json: { qr_code: create_2fa_qr_code(current_user) } end + def regenerate_api_key + current_user.regenerate_api_key! + + redirect_to edit_user_registration_path + end + + def revoke_api_key + current_user.revoke_api_key! + + redirect_to edit_user_registration_path + end + protected # Called upon creating User (before .save). Permits parameters and extracts diff --git a/app/models/user.rb b/app/models/user.rb index 535cbbf76..b1ea01d8c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -509,6 +509,21 @@ class User < ApplicationRecord .find_by(user_identities: { provider: provider_conf['provider_name'], uid: token_payload[:sub] }) end + def self.from_api_key(api_key) + where('api_key_expires_at > ?', Time.current).find_by(api_key: api_key) + end + + def regenerate_api_key! + update!( + api_key: SecureRandom.urlsafe_base64(33), + api_key_expires_at: Constants::API_KEY_EXPIRES_IN.from_now + ) + end + + def revoke_api_key! + update!(api_key: nil, api_key_expires_at: nil) + end + def has_linked_account?(provider) user_identities.exists?(provider: provider) end diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb index 2a7ba17f7..958595cea 100644 --- a/app/views/users/registrations/edit.html.erb +++ b/app/views/users/registrations/edit.html.erb @@ -35,6 +35,10 @@ <%= render partial: 'users/registrations/edit_partials/2fa' %> + + <% if Rails.application.config.x.core_api_v1_enabled %> + <%= render partial: 'users/registrations/edit_partials/api_key' %> + <% end %> diff --git a/app/views/users/registrations/edit_partials/_api_key.html.erb b/app/views/users/registrations/edit_partials/_api_key.html.erb new file mode 100644 index 000000000..bdfd1c978 --- /dev/null +++ b/app/views/users/registrations/edit_partials/_api_key.html.erb @@ -0,0 +1,24 @@ +
+ <%= t("users.registrations.edit.api_key.expired") %> +
+ <% end %> +