Initial commit
This commit is contained in:
18
app/api/v2/identity/base.rb
Normal file
18
app/api/v2/identity/base.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module V2
|
||||
module Identity
|
||||
# base api configurations for module
|
||||
class Base < Grape::API
|
||||
helpers API::V2::Identity::Utils
|
||||
|
||||
do_not_route_options!
|
||||
|
||||
mount Identity::General
|
||||
mount Identity::Sessions
|
||||
mount Identity::Users
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/api/v2/identity/general.rb
Normal file
48
app/api/v2/identity/general.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Identity
|
||||
class General < Grape::API
|
||||
desc 'Password strength testing'
|
||||
params do
|
||||
requires :password, type: String, desc: 'User password'
|
||||
end
|
||||
post '/password/validate' do
|
||||
{ entropy: PasswordStrengthChecker.calculate_entropy(params[:password]) }
|
||||
end
|
||||
|
||||
desc 'Test connectivity'
|
||||
get '/ping' do
|
||||
{ ping: 'pong' }
|
||||
end
|
||||
|
||||
desc 'Get server current unix timestamp.'
|
||||
get '/time' do
|
||||
ts = ::Time.now.to_i
|
||||
{ time: ts }
|
||||
end
|
||||
|
||||
desc 'Get barong version'
|
||||
get '/version' do
|
||||
{
|
||||
git_tag: Barong::Application::GIT_TAG,
|
||||
git_sha: Barong::Application::GIT_SHA,
|
||||
build_date: DateTime.rfc3339(Barong::Application::BUILD_DATE),
|
||||
version: Barong::Application::VERSION
|
||||
}
|
||||
end
|
||||
|
||||
desc 'Get barong configurations'
|
||||
get '/configs' do
|
||||
{
|
||||
session_expire_time: Barong::App.config.session_expire_time,
|
||||
captcha_type: Barong::App.config.captcha,
|
||||
captcha_id: (Barong::App.config.recaptcha_site_key if Barong::App.config.captcha == 'recaptcha'),
|
||||
phone_verification_type: Barong::App.config.phone_verification,
|
||||
password_min_entropy: Barong::App.config.password_min_entropy,
|
||||
password_regexp: Barong::App.config.password_regexp
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
176
app/api/v2/identity/sessions.rb
Normal file
176
app/api/v2/identity/sessions.rb
Normal file
@@ -0,0 +1,176 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_dependency 'barong/jwt'
|
||||
|
||||
module API::V2
|
||||
module Identity
|
||||
class Sessions < Grape::API
|
||||
helpers do
|
||||
def get_user(email)
|
||||
user = User.find_by(email: email)
|
||||
error!({ errors: ['identity.session.invalid_params'] }, 401) unless user
|
||||
|
||||
if user.state == 'banned'
|
||||
login_error!(reason: 'Your account is banned', error_code: 401,
|
||||
user: user.id, action: 'login', result: 'failed', error_text: 'banned')
|
||||
end
|
||||
|
||||
if user.state == 'deleted'
|
||||
login_error!(reason: 'Your account is deleted', error_code: 401,
|
||||
user: user.id, action: 'login', result: 'failed', error_text: 'deleted')
|
||||
end
|
||||
|
||||
# if user is not active or pending, then return 401
|
||||
unless user.state.in?(%w[active pending])
|
||||
login_error!(reason: 'Your account is not active', error_code: 401,
|
||||
user: user.id, action: 'login', result: 'failed', error_text: 'not_active')
|
||||
end
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Session related routes'
|
||||
resource :sessions do
|
||||
desc 'Start a new session for every LogIn',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
params do
|
||||
requires :email
|
||||
requires :password
|
||||
optional :captcha_response,
|
||||
types: { value: [String, Hash], message: 'identity.session.invalid_captcha_format' },
|
||||
desc: 'Response from captcha widget'
|
||||
optional :otp_code,
|
||||
type: String,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post do
|
||||
verify_captcha!(response: params['captcha_response'], endpoint: 'session_create')
|
||||
|
||||
declared_params = declared(params, include_missing: false)
|
||||
user = get_user(declared_params[:email])
|
||||
error!({ errors: ['identity.session.not_active'] }, 401) unless user.state == 'active'
|
||||
|
||||
unless user.authenticate(declared_params[:password])
|
||||
publish_session_failed(user)
|
||||
login_error!(reason: 'Invalid Email or Password', error_code: 401, user: user.id,
|
||||
action: 'login', result: 'failed', error_text: 'invalid_params')
|
||||
end
|
||||
|
||||
unless user.otp
|
||||
activity_record(user: user.id, action: 'login', result: 'succeed', topic: 'session')
|
||||
csrf_token = open_session(user)
|
||||
publish_session_create(user)
|
||||
|
||||
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
return status 200
|
||||
end
|
||||
|
||||
error!({ errors: ['identity.session.missing_otp'] }, 401) if declared_params[:otp_code].blank?
|
||||
unless TOTPService.validate?(user.uid, declared_params[:otp_code])
|
||||
login_error!(reason: 'OTP code is invalid', error_code: 403,
|
||||
user: user.id, action: 'login::2fa', result: 'failed', error_text: 'invalid_otp')
|
||||
end
|
||||
|
||||
activity_record(user: user.id, action: 'login::2fa', result: 'succeed', topic: 'session')
|
||||
csrf_token = open_session(user)
|
||||
publish_session_create(user)
|
||||
|
||||
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
status(200)
|
||||
end
|
||||
|
||||
desc 'Destroy current session for LogOut',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
],
|
||||
success: { code: 200, message: 'Session was destroyed' }
|
||||
delete do
|
||||
user = User.find_by(uid: session[:uid])
|
||||
error!({ errors: ['identity.session.not_found'] }, 404) unless user
|
||||
|
||||
activity_record(user: user.id, action: 'logout', result: 'succeed', topic: 'session')
|
||||
|
||||
session.destroy
|
||||
status(200)
|
||||
end
|
||||
|
||||
desc 'Auth0 authentication by id_token',
|
||||
success: { code: 200, message: 'User authenticated' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
params do
|
||||
requires :id_token,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'ID Token'
|
||||
end
|
||||
post '/auth0' do
|
||||
begin
|
||||
# Decode ID token to get user info
|
||||
claims = Barong::Auth0::JWT.verify(params[:id_token]).first
|
||||
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless claims.key?('email')
|
||||
user = User.find_by(email: claims['email'])
|
||||
|
||||
# If there is no user in platform and user email verified from id_token
|
||||
# system will create user
|
||||
if user.blank? && claims['email_verified']
|
||||
user = User.create!(email: claims['email'], state: 'active')
|
||||
user.labels.create!(scope: 'private', key: 'email', value: 'verified')
|
||||
elsif claims['email_verified'] == false
|
||||
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless user
|
||||
end
|
||||
|
||||
activity_record(user: user.id, action: 'login', result: 'succeed', topic: 'session')
|
||||
csrf_token = open_session(user)
|
||||
publish_session_create(user)
|
||||
|
||||
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
rescue StandardError => e
|
||||
report_exception(e)
|
||||
error!({ errors: ['identity.session.auth0.invalid_params'] }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Resend confirmations code(authorization code)',
|
||||
success: { code: 201, message: 'Generated verification code' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :data,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Account email or Telephone number'
|
||||
requires :action,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'for what need auth code'
|
||||
optional :channel,
|
||||
type: String,
|
||||
allow_blank: true,
|
||||
default: 'email',
|
||||
desc: 'channel that send in'
|
||||
optional :captcha_response,
|
||||
types: [String, Hash],
|
||||
desc: 'Response from captcha widget'
|
||||
end
|
||||
post '/resend' do
|
||||
current_user = User.find_by_email(params[:data])
|
||||
|
||||
return status 201 if current_user.nil?
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, params[:action])
|
||||
|
||||
status 201
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
405
app/api/v2/identity/users.rb
Normal file
405
app/api/v2/identity/users.rb
Normal file
@@ -0,0 +1,405 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_dependency 'barong/jwt'
|
||||
|
||||
module API::V2
|
||||
module Identity
|
||||
class Users < Grape::API
|
||||
helpers do
|
||||
def parse_refid!
|
||||
error!({ errors: ['identity.user.invalid_referral_format'] }, 422) unless params[:refid].start_with?(Barong::App.config.uid_prefix.upcase)
|
||||
user = User.find_by_uid(params[:refid])
|
||||
error!({ errors: ['identity.user.referral_doesnt_exist'] }, 422) if user.nil?
|
||||
|
||||
user.id
|
||||
end
|
||||
end
|
||||
|
||||
desc 'User related routes'
|
||||
resource :users do
|
||||
desc 'Creates new whitelist restriction',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: 'Whitelist restriction was created' }
|
||||
params do
|
||||
requires :whitelink_token,
|
||||
type: String,
|
||||
allow_blank: false
|
||||
end
|
||||
post '/access' do
|
||||
if Rails.cache.read(params[:whitelink_token]) == 'active'
|
||||
restriction = Restriction.new(
|
||||
category: 'whitelist',
|
||||
scope: 'ip',
|
||||
value: remote_ip,
|
||||
state: 'enabled'
|
||||
)
|
||||
|
||||
code_error!(restriction.errors.details, 422) unless restriction.save
|
||||
Rails.cache.delete('restrictions')
|
||||
else
|
||||
error!({ errors: ['identity.user.access.invalid_token'] }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Creates new user (sign up)',
|
||||
success: API::V2::Entities::UserWithFullInfo,
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :email,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User Email'
|
||||
requires :password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User Password'
|
||||
optional :refid,
|
||||
type: String,
|
||||
desc: 'Referral uid'
|
||||
optional :captcha_response,
|
||||
types: [String, Hash],
|
||||
desc: 'Response from captcha widget'
|
||||
optional :data,
|
||||
type: String,
|
||||
desc: 'Any additional key: value pairs in json string format'
|
||||
end
|
||||
post do
|
||||
verify_captcha!(response: params['captcha_response'], endpoint: 'user_create')
|
||||
|
||||
declared_params = declared(params, include_missing: false)
|
||||
user_params = declared_params.slice('email', 'password', 'data')
|
||||
|
||||
user_params[:referral_id] = parse_refid! unless params[:refid].nil?
|
||||
user = User.find_by(email: user_params[:email])
|
||||
error!({ errors: ['identity.user.active_or_banned'] }, 422) if user.present? && %w[active ban].include?(user.state)
|
||||
|
||||
if user.present?
|
||||
code_error!(user.errors.details, 422) unless user.update(user_params)
|
||||
else
|
||||
user = User.new(user_params)
|
||||
code_error!(user.errors.details, 422) unless user.save
|
||||
end
|
||||
activity_record(user: user.id, action: 'signup', result: 'succeed', topic: 'account')
|
||||
|
||||
# Creates superadmin user in first platform registration
|
||||
if Barong::App.config.first_registration_superadmin && User.count == 1
|
||||
user.update(role: 'superadmin', state: 'active')
|
||||
user.labels.create(key: 'email', value: 'verified', scope: 'private')
|
||||
else
|
||||
publish_confirmation_code(user, Barong::App.config.domain, 'sign-up')
|
||||
user.write_cache('register_email', 'true', 3600)
|
||||
end
|
||||
|
||||
csrf_token = open_session(user)
|
||||
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
end
|
||||
|
||||
desc 'Register Geetest captcha'
|
||||
get '/register_geetest' do
|
||||
CaptchaService::GeetestVerifier.new.register
|
||||
end
|
||||
|
||||
namespace :email do
|
||||
desc 'Send confirmations instructions (code in email)',
|
||||
success: { code: 201, message: 'Generated verification code' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :email,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Account email'
|
||||
optional :captcha_response,
|
||||
types: [String, Hash],
|
||||
desc: 'Response from captcha widget'
|
||||
end
|
||||
post '/generate_email_code' do
|
||||
verify_captcha!(response: params['captcha_response'], endpoint: 'email_confirmation')
|
||||
|
||||
current_user = User.find_by_email(params[:email])
|
||||
|
||||
return status 201 if current_user.nil? || current_user.active?
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, 'sign-up')
|
||||
status 201
|
||||
end
|
||||
|
||||
desc 'Confirms an account by token (one-time link in email)',
|
||||
success: API::V2::Entities::UserWithFullInfo,
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :token,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Token from email'
|
||||
end
|
||||
post '/confirm_code' do
|
||||
payload = codec.decode_and_verify(
|
||||
params[:token],
|
||||
pub_key: Barong::App.config.keystore.public_key,
|
||||
sub: 'confirmation'
|
||||
)
|
||||
current_user = User.find_by_email(payload[:email])
|
||||
|
||||
if current_user.nil? || current_user.active?
|
||||
error!({ errors: ['identity.user.active_or_doesnt_exist'] }, 422)
|
||||
end
|
||||
|
||||
token_uniq?(payload[:jti])
|
||||
current_user.labels.create!(key: 'email', value: 'verified', scope: 'private')
|
||||
csrf_token = open_session(current_user)
|
||||
|
||||
EventAPI.notify('system.user.email.confirmed',
|
||||
record: {
|
||||
user: current_user.as_json_for_event_api,
|
||||
domain: Barong::App.config.domain
|
||||
})
|
||||
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
end
|
||||
|
||||
desc 'Confirms an account by Authorization code (verify user email and achieve first label)',
|
||||
success: API::V2::Entities::UserWithFullInfo,
|
||||
failure: [
|
||||
{ code: 400, message: 'Required code are missing' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from email'
|
||||
requires :email,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'user email'
|
||||
end
|
||||
post '/confirm_email' do
|
||||
current_user = User.find_by_email(params[:email])
|
||||
|
||||
if current_user.nil? || current_user.active?
|
||||
error!({ errors: ['identity.user.active_or_doesnt_exist'] }, 422)
|
||||
end
|
||||
unless TOTPServiceAction.new('sign-up').validate?(current_user.uid, declared(params)[:code])
|
||||
error!({ errors: ['identity.user.code_invalid'] }, 422)
|
||||
end
|
||||
error!({ errors: ['identity.user.code_invalid'] }, 422) unless current_user.read_cache('register_email')
|
||||
|
||||
current_user.labels.create!(key: 'email', value: 'verified', scope: 'private')
|
||||
csrf_token = open_session(current_user)
|
||||
|
||||
EventAPI.notify('system.user.email.confirmed',
|
||||
record: {
|
||||
user: current_user.as_json_for_event_api,
|
||||
domain: Barong::App.config.domain
|
||||
})
|
||||
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# forgot reset password
|
||||
namespace :password do
|
||||
desc 'Send password reset instructions(forget password)',
|
||||
success: { code: 201, message: 'Generated password reset code' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' },
|
||||
{ code: 404, message: 'User doesn\'t exist'}
|
||||
]
|
||||
params do
|
||||
requires :email,
|
||||
type: String,
|
||||
message: 'identity.user.missing_email',
|
||||
allow_blank: false,
|
||||
desc: 'Account email'
|
||||
optional :captcha_response,
|
||||
types: [String, Hash],
|
||||
desc: 'Response from captcha widget'
|
||||
end
|
||||
|
||||
post '/generate_code' do
|
||||
verify_captcha!(response: params['captcha_response'], endpoint: 'password_reset')
|
||||
|
||||
current_user = User.find_by_email(params[:email])
|
||||
return status 404 if current_user.nil?
|
||||
|
||||
activity_record(user: current_user.id, action: 'request password reset', result: 'succeed', topic: 'password')
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, 'reset-password')
|
||||
status 201
|
||||
end
|
||||
|
||||
desc 'Validate reset code or set a new password via one-time email link',
|
||||
success: { code: 201, message: 'reset code is ok' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are missing' },
|
||||
{ code: 422, message: 'Validation errors' },
|
||||
{ code: 404, message: 'User doesn\'t exist'}
|
||||
]
|
||||
params do
|
||||
optional :email,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Account email'
|
||||
optional :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Confirm code'
|
||||
optional :reset_password_token,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Token from email'
|
||||
optional :password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User new password'
|
||||
optional :confirm_password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User new password'
|
||||
optional :captcha_response,
|
||||
types: [String, Hash],
|
||||
desc: 'Response from captcha widget'
|
||||
end
|
||||
|
||||
post '/confirm_code' do
|
||||
token_reset_request = params[:reset_password_token].present? ||
|
||||
params[:password].present? ||
|
||||
params[:confirm_password].present?
|
||||
|
||||
if token_reset_request
|
||||
error!({ errors: ['identity.user.missing_pass_token', 'identity.user.empty_reset_password_token'] }, 422) if params[:reset_password_token].blank?
|
||||
error!({ errors: ['identity.user.missing_password', 'identity.user.empty_password'] }, 422) if params[:password].blank?
|
||||
error!({ errors: ['identity.user.missing_confirm_password', 'identity.user.empty_confirm_password'] }, 422) if params[:confirm_password].blank?
|
||||
error!({ errors: ['identity.user.passwords_doesnt_match'] }, 422) unless params[:password] == params[:confirm_password]
|
||||
|
||||
payload = codec.decode_and_verify(
|
||||
params[:reset_password_token],
|
||||
pub_key: Barong::App.config.keystore.public_key,
|
||||
sub: 'reset'
|
||||
)
|
||||
|
||||
if Rails.cache.read("reset_password_#{payload[:email]}") != payload[:reset_token] ||
|
||||
Rails.cache.read(payload[:jti]) == 'utilized'
|
||||
error!({ errors: ['identity.user.utilized_token'] }, 422)
|
||||
end
|
||||
|
||||
current_user = User.find_by_email(payload[:email])
|
||||
return status 404 if current_user.nil?
|
||||
|
||||
unless PasswordStrengthChecker.validate!(params[:password]) == 'strong'
|
||||
error!({ errors: ["resource.password.#{PasswordStrengthChecker.validate!(params[:password])}"] }, 422)
|
||||
end
|
||||
|
||||
unless current_user.update(password: params[:password])
|
||||
error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json
|
||||
activity_record(user: current_user.id, action: 'password reset',
|
||||
result: 'failed', topic: 'password', data: error_note)
|
||||
code_error!(current_user.errors.details, 422)
|
||||
end
|
||||
|
||||
Rails.cache.delete("reset_password_#{payload[:email]}")
|
||||
Rails.cache.write(payload[:jti], 'utilized', expires_in: Barong::App.config.jwt_expire_time.seconds)
|
||||
activity_record(user: current_user.id, action: 'password reset', result: 'succeed', topic: 'password')
|
||||
|
||||
EventAPI.notify('system.user.password.reset',
|
||||
record: {
|
||||
user: current_user.as_json_for_event_api,
|
||||
domain: Barong::App.config.domain
|
||||
})
|
||||
status 201
|
||||
else
|
||||
error!({ errors: ['identity.user.missing_email', 'identity.user.empty_email'] }, 422) if params[:email].blank?
|
||||
error!({ errors: ['identity.user.missing_code', 'identity.user.empty_code'] }, 422) if params[:code].blank?
|
||||
verify_captcha!(response: params['captcha_response'], endpoint: 'password_reset')
|
||||
|
||||
current_user = User.find_by_email(params[:email])
|
||||
return status 404 if current_user.nil?
|
||||
|
||||
totp = TOTPServiceAction.new('reset-password')
|
||||
error!({ errors: ['resource.totp.code'] }, 422) unless totp.safe_validate?(current_user.uid, declared(params)[:code])
|
||||
|
||||
status 200
|
||||
end
|
||||
end
|
||||
|
||||
# reset forgot password
|
||||
desc 'Sets new account password(for forgot password)',
|
||||
success: { code: 201, message: 'Resets password' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :email,
|
||||
type: String,
|
||||
message: 'identity.user.email',
|
||||
allow_blank: false,
|
||||
desc: 'user email'
|
||||
requires :code,
|
||||
type: String,
|
||||
message: 'identity.user.missing_pass_token',
|
||||
allow_blank: false,
|
||||
desc: 'code from email again'
|
||||
requires :password,
|
||||
type: String,
|
||||
message: 'identity.user.missing_password',
|
||||
allow_blank: false,
|
||||
desc: 'User new password'
|
||||
requires :confirm_password,
|
||||
type: String,
|
||||
message: 'identity.user.missing_confirm_password',
|
||||
allow_blank: false,
|
||||
desc: 'User new password'
|
||||
end
|
||||
post '/reset' do
|
||||
current_user = User.find_by_email(params[:email])
|
||||
return status 404 if current_user.nil?
|
||||
|
||||
totp = TOTPServiceAction.new('reset-password')
|
||||
error!({ errors: ['resource.totp.code'] }, 422) unless totp.safe_validate?(current_user.uid, declared(params)[:code])
|
||||
|
||||
unless params[:password] == params[:confirm_password]
|
||||
error!({ errors: ['identity.user.passwords_doesnt_match'] }, 422)
|
||||
end
|
||||
|
||||
unless PasswordStrengthChecker.validate!(params[:password]) == 'strong'
|
||||
error!({ errors: ["resource.password.#{PasswordStrengthChecker.validate!(temp_password)}"] }, 422)
|
||||
end
|
||||
|
||||
unless current_user.update(password: params[:password])
|
||||
error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json
|
||||
activity_record(user: current_user.id, action: 'password reset',
|
||||
result: 'failed', topic: 'password', data: error_note)
|
||||
code_error!(current_user.errors.details, 422)
|
||||
end
|
||||
|
||||
totp.validate?(current_user.uid, declared(params)[:code])
|
||||
activity_record(user: current_user.id, action: 'password reset', result: 'succeed', topic: 'password')
|
||||
|
||||
EventAPI.notify('system.user.password.reset',
|
||||
record: {
|
||||
user: current_user.as_json_for_event_api,
|
||||
domain: Barong::App.config.domain
|
||||
})
|
||||
status 201
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
161
app/api/v2/identity/utils.rb
Normal file
161
app/api/v2/identity/utils.rb
Normal file
@@ -0,0 +1,161 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Identity
|
||||
module Utils
|
||||
def session
|
||||
request.session
|
||||
end
|
||||
|
||||
def codec
|
||||
@_codec ||= Barong::JWT.new(key: Barong::App.config.keystore.private_key)
|
||||
end
|
||||
|
||||
def open_session(user)
|
||||
csrf_token = SecureRandom.hex(10)
|
||||
session.merge!(
|
||||
"uid": user.uid,
|
||||
"user_ip": remote_ip,
|
||||
"user_agent": request.env['HTTP_USER_AGENT'],
|
||||
"expire_time": Time.now.to_i + Barong::App.config.session_expire_time,
|
||||
"csrf_token": csrf_token
|
||||
)
|
||||
|
||||
csrf_token
|
||||
end
|
||||
|
||||
def verify_captcha!(response:, endpoint:, error_statuses: [400, 422])
|
||||
# by default we protect user_create session_create password_reset email_confirmation endpoints
|
||||
return unless BarongConfig.list['captcha_protected_endpoints']&.include?(endpoint)
|
||||
|
||||
case Barong::App.config.captcha
|
||||
when 'recaptcha'
|
||||
recaptcha(response: response)
|
||||
when 'geetest'
|
||||
geetest(response: response)
|
||||
end
|
||||
end
|
||||
|
||||
def recaptcha(response:, error_statuses: [400, 422])
|
||||
error!({ errors: ['identity.captcha.required'] }, error_statuses.first) if response.blank?
|
||||
|
||||
captcha_error_message = 'identity.captcha.verification_failed'
|
||||
|
||||
if CaptchaService::RecaptchaVerifier.new(request: request).response_valid?(skip_remote_ip: true, response: response)
|
||||
return
|
||||
end
|
||||
|
||||
error!({ errors: [captcha_error_message] }, error_statuses.last)
|
||||
rescue StandardError
|
||||
error!({ errors: [captcha_error_message] }, error_statuses.last)
|
||||
end
|
||||
|
||||
def geetest(response:, error_statuses: [400, 422])
|
||||
error!({ errors: ['identity.captcha.required'] }, error_statuses.first) if response.blank?
|
||||
|
||||
geetest_error_message = 'identity.captcha.verification_failed'
|
||||
validate_geetest_response(response: response)
|
||||
|
||||
return if CaptchaService::GeetestVerifier.new.validate(response)
|
||||
|
||||
error!({ errors: [geetest_error_message] }, error_statuses.last)
|
||||
rescue StandardError
|
||||
error!({ errors: [geetest_error_message] }, error_statuses.last)
|
||||
end
|
||||
|
||||
def validate_geetest_response(response:)
|
||||
unless (response['geetest_challenge'].is_a? String) &&
|
||||
(response['geetest_validate'].is_a? String) &&
|
||||
(response['geetest_seccode'].is_a? String)
|
||||
error!({ errors: ['identity.captcha.mandatory_fields'] }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
def login_error!(options = {})
|
||||
options[:data] = { reason: options[:reason] }.to_json
|
||||
options[:topic] = 'session'
|
||||
activity_record(options.except(:reason, :error_code, :error_text))
|
||||
error!({ errors: ['identity.session.' + options[:error_text]] }, options[:error_code])
|
||||
end
|
||||
|
||||
def activity_record(options = {})
|
||||
params = {
|
||||
category: 'user',
|
||||
user_id: options[:user],
|
||||
user_ip: remote_ip,
|
||||
user_agent: request.env['HTTP_USER_AGENT'],
|
||||
topic: options[:topic],
|
||||
action: options[:action],
|
||||
result: options[:result],
|
||||
data: options[:data]
|
||||
}
|
||||
Activity.create(params)
|
||||
end
|
||||
|
||||
def token_uniq?(jti)
|
||||
error!({ errors: ['identity.user.utilized_token'] }, 422) if Rails.cache.read(jti) == 'utilized'
|
||||
Rails.cache.write(jti, 'utilized', expires_in: Barong::App.config.jwt_expire_time.seconds)
|
||||
end
|
||||
|
||||
def publish_confirmation(user, domain)
|
||||
token = codec.encode(sub: 'confirmation', email: user.email, uid: user.uid)
|
||||
EventAPI.notify(
|
||||
'system.user.email.confirmation.token',
|
||||
record: {
|
||||
user: user.as_json_for_event_api,
|
||||
domain: domain,
|
||||
token: token
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def publish_confirmation_code(user, domain, action)
|
||||
totp = TOTPServiceAction.new(action)
|
||||
totp.create(user.uid, user.email)
|
||||
record = {
|
||||
user: user.as_json_for_event_api,
|
||||
domain: domain,
|
||||
code: totp.read_code(user.uid)
|
||||
}
|
||||
|
||||
case action
|
||||
when 'sign-up'
|
||||
record[:token] = codec.encode(sub: 'confirmation', email: user.email, uid: user.uid)
|
||||
when 'reset-password'
|
||||
reset_token = SecureRandom.hex(10)
|
||||
Rails.cache.write(
|
||||
"reset_password_#{user.email}",
|
||||
reset_token,
|
||||
expires_in: Barong::App.config.jwt_expire_time.seconds
|
||||
)
|
||||
record[:token] = codec.encode(
|
||||
sub: 'reset',
|
||||
email: user.email,
|
||||
uid: user.uid,
|
||||
reset_token: reset_token
|
||||
)
|
||||
end
|
||||
|
||||
EventAPI.notify(action, record: record)
|
||||
end
|
||||
|
||||
def publish_session_create(user)
|
||||
EventAPI.notify('system.session.create',
|
||||
record: {
|
||||
user: user.as_json_for_event_api,
|
||||
user_ip: remote_ip,
|
||||
user_agent: request.env['HTTP_USER_AGENT']
|
||||
})
|
||||
end
|
||||
|
||||
def publish_session_failed(user)
|
||||
EventAPI.notify('system.session.failed',
|
||||
record: {
|
||||
user: user.as_json_for_event_api,
|
||||
user_ip: remote_ip,
|
||||
user_agent: request.env['HTTP_USER_AGENT']
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user