# frozen_string_literal: true module API::V2 module Resource class Users < Grape::API helpers ::API::V2::NamedParams helpers do def password_error!(options = {}) options[:topic] = 'password' record_error!(options) end def validate_topic!(topic) unless %w[all session otp password account].include?(topic) error!({ errors: ['resource.user.wrong_topic'] }, 422) end end def verify_otp! error!({ errors: ['resource.user.missing_otp_code'] }, 422) if params[:otp_code].nil? error!({ errors: ['resource.user.empty_otp_code'] }, 422) if params[:otp_code].empty? unless TOTPService.validate?(current_user.uid, params[:otp_code]) error!({ errors: ['resource.user.invalid_otp'] }, 422) end end end resource :users do desc 'Returns current user', success: API::V2::Entities::UserWithFullInfo get '/me' do present current_user, with: API::V2::Entities::UserWithFullInfo end desc 'Updates current user data field', success: API::V2::Entities::UserWithFullInfo params do requires :data, type: String, allow_blank: false, desc: 'Any additional key: value pairs in json string format' end put '/me' do code_error!(current_user.errors.details, 422) unless current_user.update(data: params[:data]) present current_user, with: API::V2::Entities::UserWithFullInfo end desc 'Blocks current user', success: { code: 200, message: 'Current user was blocked' } params do requires :password, type: String, allow_blank: false, desc: 'Account password' optional :otp_code, type: String, allow_blank: false, desc: 'Code from Google Authenticator' end delete '/me' do error!({ errors: ['resource.user.invalid_password'] }, 422) unless password_valid?(params[:password]) verify_otp! if current_user.otp current_user.labels.create(key: 'delete', value: 'by_user', scope: 'private') EventAPI.notify( 'system.user.account.deleted', record: { user: current_user.as_json_for_event_api } ) status(200) end desc 'Returns user activity', success: Entities::Activity params do requires :topic, type: String, allow_blank: { value: false, message: 'resource.user.empty_topic' }, desc: 'Topic of user activity. Allowed: [all, password, session, otp]' optional :time_from, type: { value: Integer, message: 'resource.user.non_integer_time_from' }, allow_blank: { value: false, message: 'resource.user.empty_time_from' }, desc: 'An integer represents the seconds elapsed since Unix epoch.'\ 'If set, only activities created after the time will be returned.' optional :time_to, type: { value: Integer, message: 'resource.user.non_integer_time_to' }, allow_blank: { value: false, message: 'resource.user.empty_time_to' }, desc: 'An integer represents the seconds elapsed since Unix epoch.'\ 'If set, only activities created before the time will be returned.' optional :result, type: { value: String, message: 'resource.user.non_string_result' }, allow_blank: { value: false, message: 'resource.user.empty_result' }, desc: 'Result of user activity. Allowed: [succeed, failed, denied]' use :pagination_filters end get '/activity/:topic' do validate_topic!(params[:topic]) data = current_user.activities.order('id DESC') data = data.where(topic: params[:topic]) if params[:topic] != 'all' data = data.tap { |q| q.where!('created_at >= ?', Time.at(params[:time_from])) if params[:time_from] } .tap { |q| q.where!('created_at < ?', Time.at(params[:time_to])) if params[:time_to] } .tap { |q| q.where!(result: params[:result]) if params[:result] } error!({ errors: ['resource.user.no_activity'] }, 422) unless data.present? present paginate(data), with: Entities::Activity end # change password desc 'Sets new account password(change password)', success: { code: 201, message: 'Code sent for confirm password' }, failure: [ { code: 400, message: 'Required params are empty' }, { code: 404, message: 'Record is not found' }, { code: 422, message: 'Validation errors' } ] params do requires :old_password, type: String, allow_blank: false, desc: 'Previous account password' requires :new_password, type: String, allow_blank: false, desc: 'User password' requires :confirm_password, type: String, allow_blank: false, desc: 'User password' end put '/password' do # if user inters too many wrong password, we logout him attempt = current_user.read_cache('attempt_change_password') || 0 # TODO: numeric must be config that set by admin if attempt >= User::CHANGE_PASS_ATTEMPTS activity_record(user: current_user.id, action: 'many_change_password_attempts', result: 'succeed', topic: 'session') current_user.delete_cache('attempt_change_password') request.session.destroy password_error!(reason: 'Too many attempts', error_code: 422, user: current_user.id, action: 'password change', error_text: 'many_change_password_attempts') end unless params[:new_password] == params[:confirm_password] password_error!(reason: 'New passwords don\'t match', error_code: 422, user: current_user.id, action: 'password change', error_text: 'doesnt_match') end unless PasswordStrengthChecker.validate!(params[:new_password]) == 'strong' password_error!(reason: 'Previous password is not strong', error_code: 400, user: current_user.id, action: 'password change', error_text: 'password_is_not_strong') end unless password_valid?(params[:old_password]) current_user.write_cache('attempt_change_password', attempt + 1, 86_400) password_error!(reason: 'Previous password is not correct', error_code: 400, user: current_user.id, action: 'password change', error_text: 'prev_pass_not_correct') end if params[:old_password] == params[:new_password] password_error!(reason: 'New password cant be the same, as old one', error_code: 400, user: current_user.id, action: 'password change', error_text: 'no_change_provided') end publish_confirmation_code(current_user, Barong::App.config.domain, 'change-password') current_user.write_cache('change_password', params[:new_password], 300) status 201 end desc 'Confirm account password(enter authorization code)', success: { code: 201, message: 'Changes password' }, failure: [ { code: 400, message: 'Required params are empty' }, { code: 404, message: 'Record is not found' }, { code: 422, message: 'Validation errors' } ] params do requires :code, type: String, allow_blank: false, desc: 'Totp Code from email' end post '/confirm_password' do temp_password = current_user.read_cache('change_password') error!({ errors: ['resource.password.email_expired'] }, 422) if temp_password.blank? unless TOTPServiceAction.new('change-password').validate?(current_user.uid, declared(params)[:code]) error!({ errors: ['resource.password.confirm_code'] }, 422) end unless current_user.update(password: temp_password) error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json activity_record(user: current_user.id, action: 'password change', result: 'failed', topic: 'password', data: error_note) code_error!(current_user.errors.details, 422) end activity_record(user: current_user.id, action: 'password change', result: 'succeed', topic: 'password') EventAPI.notify('system.user.password.change', record: { user: current_user.as_json_for_event_api, domain: Barong::App.config.domain }) current_user.delete_cache('change_password') status 201 end end end end end