mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-03-04 19:53:19 +08:00
Implement API key auth and generation [SCI-6968]
This commit is contained in:
parent
19442b6157
commit
c5b215af32
11 changed files with 141 additions and 0 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="api-key-container">
|
||||
<h3 class="title"><%= t("users.registrations.edit.api_key.title") %></h3>
|
||||
<div class="description">
|
||||
<%= t("users.registrations.edit.api_key.description") %>
|
||||
</div>
|
||||
<% if current_user.api_key %>
|
||||
<div class="api-key-display">
|
||||
<%= text_field_tag :api_key, current_user.api_key, class: "api-key-field", disabled: "disabled" %>
|
||||
<% if current_user.api_key_expires_at < Time.current %>
|
||||
<p class="api-key-error">
|
||||
<%= t("users.registrations.edit.api_key.expired") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="api-key-controls">
|
||||
<% if current_user.api_key %>
|
||||
<%= button_to t("users.registrations.edit.api_key.regenerate"), users_api_key_regenerate_path, class: "btn btn-primary" %>
|
||||
<%= button_to t("users.registrations.edit.api_key.revoke"), users_api_key_revoke_path, class: "btn btn-danger" %>
|
||||
<% else %>
|
||||
<%= button_to t("users.registrations.edit.api_key.generate"), users_api_key_regenerate_path, class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -247,6 +247,8 @@ class Constants
|
|||
TWO_FACTOR_RECOVERY_CODE_COUNT = 6
|
||||
TWO_FACTOR_RECOVERY_CODE_LENGTH = 12
|
||||
|
||||
API_KEY_EXPIRES_IN = 1.year
|
||||
|
||||
#=============================================================================
|
||||
# Protocol importers
|
||||
#=============================================================================
|
||||
|
|
|
@ -2825,6 +2825,13 @@ en:
|
|||
validation: "Are you sure you want to remove it?"
|
||||
cancel: "Cancel"
|
||||
remove: "Remove"
|
||||
api_key:
|
||||
title: "API Key"
|
||||
description: "Generate or revoke an API key for use with the SciNote API. Regenerating the API key will invalidate the old one."
|
||||
generate: "Generate"
|
||||
regenerate: "Regenerate"
|
||||
revoke: "Revoke"
|
||||
expired: "This key has expired!"
|
||||
new:
|
||||
head_title: "Sign up"
|
||||
team_name_label: "Team name"
|
||||
|
@ -4038,6 +4045,8 @@ en:
|
|||
api:
|
||||
core:
|
||||
status_ok: "Ok"
|
||||
invalid_api_key: "API key is invalid or expired"
|
||||
invalid_api_key_detail: "The API key you are using does not exist or has expired."
|
||||
expired_token: "Token is expired"
|
||||
invalid_token: "Token is invalid"
|
||||
missing_token: "Core: No token in the header"
|
||||
|
|
|
@ -852,6 +852,9 @@ Rails.application.routes.draw do
|
|||
post 'users/2fa_enable' => 'users/registrations#two_factor_enable'
|
||||
post 'users/2fa_disable' => 'users/registrations#two_factor_disable'
|
||||
get 'users/2fa_qr_code' => 'users/registrations#two_factor_qr_code'
|
||||
|
||||
post 'users/api_key_regenerate' => 'users/registrations#regenerate_api_key'
|
||||
post 'users/api_key_revoke' => 'users/registrations#revoke_api_key'
|
||||
end
|
||||
|
||||
namespace :api, defaults: { format: 'json' } do
|
||||
|
|
10
db/migrate/20220712110253_add_api_key_to_users.rb
Normal file
10
db/migrate/20220712110253_add_api_key_to_users.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddApiKeyToUsers < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
change_table :users, bulk: true do |t|
|
||||
t.string :api_key
|
||||
t.datetime :api_key_expires_at
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue