## # Application controller for Anubis library. class Anubis::Core::ApplicationController < ActionController::API prepend_before_action :anubis_core_initialization #include AbstractController::Translation include ActionController::MimeResponds #include ActionController::Parameters #include ActionDispatch::Http::Parameters #include ActionDispatch::Request # @!attribute [rw] version # @return [Integer] Specifies the api version. Parameters receive from URL (defaults to: 0). attr_accessor :version # @!attribute [rw] locale # @return [String] Specifies the current language locale (defaults to: 'ru'). # Parameters receive from URL or user definition attr_accessor :locale # @!attribute [rw] current_user # @return [ActiveRecord] Specifies current user (defaults to: nil). attr_accessor :current_user # @!attribute [rw] output # @return [Anubis::Output] standard output. attr_accessor :output # @!attribute [rw] writer # @return [Object] Specifies access of current user to this controller (defaults to: false). attr_accessor :writer # @!attribute [rw] etc # @return [Anubis::Etc::Base] global system parameters attr_accessor :etc # @!attribute [rw] exports # @return [Anubis::Export] Export data class attr_accessor :exports ## # Returns redis database class def redis @redis ||= Redis.new end ## # Sets default parameters for application controller. def anubis_core_initialization self.version = 0 if defined? params self.etc = Anubis::Etc::Base.new({ params: params }) else self.etc = Anubis::Etc::Base.new end self.output = nil self.exports = nil self.writer = false self.current_user = nil self.locale = params[:locale] if params.has_key? :locale self.locale = 'ru' unless self.locale self.locale = 'ru' if self.locale == '' begin I18n.locale = self.locale rescue I18n.locale = 'ru' end return if request.method == 'OPTIONS' if !params.has_key? :version self.error_exit({ error: I18n.t('errors.no_api_version') }) return end if self.access_allowed? self.set_access_control_headers else self.error_exit({ error: I18n.t('errors.access_not_allowed') }) end self.version = params[:version] if self.authenticate? if self.authentication if self.check_menu_access? return if !self.menu_access params[:controller] end end end #self.user_time_zone if self.current_user Time.zone = self.current_user.timezone if self.current_user self.after_initialization end ## # Calls after first controller initialization def after_initialization end ## # Gracefully terminate script execution with code 422 (Unprocessable entity). And JSON data # @param data [Hash] Resulting data # @option data [Integer] :code resulting error code # @option data [String] :error resulting error message def error_exit(data) result = { result: -1, message: 'Error' } result[:result] = data[:code] if data.has_key? :code result[:message] = data[:error] if data.has_key? :error respond_to do |format| format.json { render json: result, status: :unprocessable_entity } end begin exit rescue SystemExit => e end end ## # Get current user model # @return [ActiveRecord] defined user model. It is used for get current user data. May be redefined when user model is changed def get_user_model nil end ## # Get current user model filed json exception # @return [Array] defined user exception for to_json function def get_user_model_except [] end ## # @!group Block of authorization ## # Checks if needed user authentication. # @return [Boolean] if true, then user must be authenticated. def authenticate? return true end ## # Authenticates user in the system def authentication if !self.token self.error_exit({ error: I18n.t('errors.authentication_required') }) return false end # Check session presence session = self.redis.get(self.redis_prefix + 'session:' + self.token) if !session self.error_exit({ error: I18n.t('errors.session_expired') }) return false end session = JSON.parse(session, { symbolize_names: true }) if !session.has_key?(:uuid) || !session.has_key?(:ttl) self.error_exit({ error: I18n.t('errors.session_expired') }) return false end if session[:ttl] < Time.now self.error_exit({ error: I18n.t('errors.session_expired') }) self.redis.del(self.redis_prefix + 'session:' + self.token) return false end # Load user data from redis database user_json = self.redis.get(self.redis_prefix + 'user:' + session[:uuid]) if !user_json # Check user presence based on session user UUID user = self.get_user_model.where(uuid_bin: self.uuid_to_bin(session[:uuid])).first if !user self.error_exit({ error: I18n.t('errors.authentication_required') }) return false end user_json = self.redis_save_user user end begin self.current_user = self.get_user_model.new(JSON.parse(user_json,{ symbolize_names: true })) rescue self.current_user = nil end if !self.current_user self.error_exit({ error: I18n.t('errors.authentication_required') }) return false end session[:time] = Time.now session[:ttl] = session[:time] + self.current_user.timeout self.redis.set(self.redis_prefix + 'session:' + self.token, session.to_json) true end ## # Checks user must have access for current controller. # @return [Boolean] if true, then user must have access for this controller. def check_menu_access? true end ## # Check menu access for current user of current controller # @return [Boolean] if true, then user have access for this controller. def menu_access(controller, exit = true) self.writer = true true end ## # Get current token based on HTTP Authorization # @return [String] current token def token if Rails.env.development? return params[:token] if params[:token] end request.env.fetch('HTTP_AUTHORIZATION', '').scan(/Bearer (.*)$/).flatten.last end # @!endgroup ## # Check access for API. # @return [Boolean] access for requested client def access_allowed? allowed_sites = [request.env['HTTP_ORIGIN']] allowed_sites.include?(request.env['HTTP_ORIGIN']) end ## # Set allow header information for multi-domain requests. Requested for browsers when API is not in the same # address as Frontend application. def set_access_control_headers headers['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN'] headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS, DELETE, PUT, PATCH' headers['Access-Control-Max-Age'] = '1000' headers['Access-Control-Allow-Headers'] = '*,x-requested-with,Content-Type,Authorization' end ## # @!group Block of UUID functions ## # Decodes binary UUID data into the UUID string # @param data [Binary] binary representation of UUID # @return [String, nil] string representation of UUID or nil if can't be decoded def bin_to_uuid(data) begin data = data.unpack('H*')[0] return data[0..7]+'-'+data[8..11]+'-'+data[12..15]+'-'+data[16..19]+'-'+data[20..31] rescue return nil end end ## # Encodes string UUID data into the binary UUID # @param data [Binary] string representation of UUID # @return [Binary, nil] binary representation of UUID or nil if can't be encoded def uuid_to_bin(data) begin return [data.delete('-')].pack('H*') rescue return nil end end ## # Generates new UUID data # @return [String] string representation of UUID def new_uuid SecureRandom.uuid end ## # Generates new session ID # @return [string] string representation of session (64 bytes) def new_session_id SecureRandom.hex(32) end # @!endgroup ## # Saves user data into redis database and returns user JSON representation # @param user [ActiveRecord] current user data # @return [String] JSON representation of user data def redis_save_user(user) user_json = user.to_json(except: self.get_user_model_except) user_hash = JSON.parse user_json, { symbolize_names: true } user_hash[:uuid] = user.uuid user_json = user_hash.to_json self.redis.set(self.redis_prefix + 'user:' + user.uuid, user_json) user_json end ## # Returns defined application prefix for redis cache for controller. Default value '' def redis_prefix begin value = Rails.configuration.redis_prefix rescue return '' end value + ':' end ## # Returns default defined locale def default_locale Rails.configuration.i18n.default_locale.to_s end ## # Default route for OPTIONS method def options if self.access_allowed? self.set_access_control_headers head :ok else head :forbidden end end end