├── test ├── dummy │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── reference.rb │ │ │ ├── date_field.rb │ │ │ ├── float_field.rb │ │ │ ├── string_field.rb │ │ │ ├── boolean_field.rb │ │ │ ├── decimal_field.rb │ │ │ ├── integer_field.rb │ │ │ ├── owner.rb │ │ │ ├── tree.rb │ │ │ ├── serialize_field.rb │ │ │ ├── polymorphic_field.rb │ │ │ ├── has_many_class_name_field.rb │ │ │ ├── belongs_to_class_name_field.rb │ │ │ ├── has_and_belongs_to_many_field.rb │ │ │ ├── has_many_field.rb │ │ │ ├── has_many_through_field.rb │ │ │ ├── belongs_to_field.rb │ │ │ └── has_one_field.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── application_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── config │ │ ├── routes.rb │ │ ├── storage.yml │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── secrets.yml │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ └── test.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── config.ru │ ├── db │ │ └── migrate │ │ │ ├── 20160627172810_create_owner.rb │ │ │ ├── 20181111162121_create_references_table.rb │ │ │ ├── 20150608130516_create_date_field.rb │ │ │ ├── 20150608131610_create_float_field.rb │ │ │ ├── 20150608131430_create_integer_field.rb │ │ │ ├── 20150608131603_create_decimal_field.rb │ │ │ ├── 20150608132159_create_boolean_field.rb │ │ │ ├── 20150608132621_create_string_field.rb │ │ │ ├── 20150814081918_create_has_many_through_field.rb │ │ │ ├── 20170614141921_create_serialize_field.rb │ │ │ ├── 20150623115554_create_has_many_class_name_field.rb │ │ │ ├── 20160627172951_create_tree.rb │ │ │ ├── 20150612112520_create_has_and_belongs_to_many_field.rb │ │ │ ├── 20150608150016_create_has_many_field.rb │ │ │ ├── 20150616150629_create_polymorphic_field.rb │ │ │ ├── 20150608133044_create_has_one_field.rb │ │ │ ├── 20150609114636_create_belongs_to_class_name_field.rb │ │ │ ├── 20160628173505_add_timestamps.rb │ │ │ └── 20150608133038_create_belongs_to_field.rb │ ├── Rakefile │ └── README.rdoc ├── fixtures │ ├── reference.yml │ ├── serialize_field.yml │ ├── has_many_through_field.yml │ ├── owner.yml │ ├── has_many_field.yml │ ├── tree.yml │ ├── string_field.yml │ └── has_one_field.yml ├── forest_liana_test.rb ├── test_helper.rb └── services │ └── forest_liana │ └── operator_date_interval_parser_test.rb ├── .rspec ├── README.rdoc ├── app ├── assets │ ├── images │ │ └── forest_liana │ │ │ └── .keep │ ├── javascripts │ │ └── forest_liana │ │ │ └── application.js │ └── stylesheets │ │ ├── forest_liana │ │ └── application.css │ │ └── scaffold.css ├── helpers │ └── forest_liana │ │ ├── application_helper.rb │ │ ├── schema_helper.rb │ │ ├── decoration_helper.rb │ │ ├── widgets_helper.rb │ │ ├── adapter_helper.rb │ │ └── query_helper.rb ├── controllers │ └── forest_liana │ │ ├── apimaps_controller.rb │ │ ├── devise_controller.rb │ │ ├── scopes_controller.rb │ │ ├── mixpanel_controller.rb │ │ ├── intercom_controller.rb │ │ ├── base_controller.rb │ │ └── router.rb ├── services │ └── forest_liana │ │ ├── objective_stat_getter.rb │ │ ├── stripe_base_getter.rb │ │ ├── ability │ │ ├── exceptions │ │ │ ├── access_denied.rb │ │ │ ├── unknown_collection.rb │ │ │ ├── trigger_forbidden.rb │ │ │ ├── action_condition_error.rb │ │ │ └── require_approval.rb │ │ ├── fetch.rb │ │ └── permission │ │ │ └── request_permission.rb │ │ ├── oidc_configuration_retriever.rb │ │ ├── stripe_payment_refunder.rb │ │ ├── integration_base_getter.rb │ │ ├── utils │ │ ├── beta_schema_utils.rb │ │ ├── context_variables.rb │ │ └── context_variables_injector.rb │ │ ├── has_many_associator.rb │ │ ├── stripe_subscription_getter.rb │ │ ├── stat_getter.rb │ │ ├── resource_getter.rb │ │ ├── stripe_source_getter.rb │ │ ├── stripe_payment_getter.rb │ │ ├── intercom_conversation_getter.rb │ │ ├── live_query_checker.rb │ │ ├── intercom_attributes_getter.rb │ │ ├── token.rb │ │ ├── stripe_invoice_getter.rb │ │ ├── belongs_to_updater.rb │ │ ├── controller_factory.rb │ │ ├── resource_updater.rb │ │ ├── forest_api_requester.rb │ │ ├── has_many_dissociator.rb │ │ ├── authorization_getter.rb │ │ ├── ip_whitelist.rb │ │ ├── ability.rb │ │ ├── oidc_client_manager.rb │ │ ├── value_stat_getter.rb │ │ ├── intercom_conversations_getter.rb │ │ ├── line_stat_getter.rb │ │ ├── authentication.rb │ │ ├── stripe_sources_getter.rb │ │ ├── leaderboard_stat_getter.rb │ │ ├── stripe_subscriptions_getter.rb │ │ ├── resource_creator.rb │ │ ├── oidc_dynamic_client_registrator.rb │ │ ├── stripe_payments_getter.rb │ │ ├── stripe_invoices_getter.rb │ │ ├── ip_whitelist_checker.rb │ │ ├── base_getter.rb │ │ ├── smart_action_field_validator.rb │ │ ├── pie_stat_getter.rb │ │ └── mixpanel_last_events_getter.rb ├── views │ └── layouts │ │ └── forest_liana │ │ └── application.html.erb ├── models │ └── forest_liana │ │ └── model │ │ ├── stat.rb │ │ ├── segment.rb │ │ └── collection.rb └── serializers │ └── forest_liana │ ├── mixpanel_event_serializer.rb │ ├── stat_serializer.rb │ ├── stripe_payment_serializer.rb │ ├── stripe_bank_account_serializer.rb │ ├── intercom_conversation_serializer.rb │ ├── stripe_card_serializer.rb │ ├── stripe_subscription_serializer.rb │ ├── stripe_invoice_serializer.rb │ ├── intercom_attribute_serializer.rb │ └── schema_serializer.rb ├── spec ├── dummy │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_controller.rb │ │ │ └── forest │ │ │ │ └── islands_controller.rb │ │ ├── models │ │ │ ├── town.rb │ │ │ ├── driver.rb │ │ │ ├── car.rb │ │ │ ├── reference.rb │ │ │ ├── location.rb │ │ │ ├── manufacturer.rb │ │ │ ├── application_record.rb │ │ │ ├── sub_application_record.rb │ │ │ ├── owner.rb │ │ │ ├── address.rb │ │ │ ├── user_record.rb │ │ │ ├── garage_record.rb │ │ │ ├── product.rb │ │ │ ├── island.rb │ │ │ ├── user.rb │ │ │ └── tree.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── config │ │ │ └── routes.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── config │ │ ├── storage.yml │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── forest_liana.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── database.yml │ │ ├── secrets.yml │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ └── test.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── db │ │ └── migrate │ │ │ ├── 20190716130830_add_age_to_tree.rb │ │ │ ├── 20190716135241_add_type_to_user.rb │ │ │ ├── 20210526084712_create_products.rb │ │ │ ├── 20210326110524_create_references.rb │ │ │ ├── 20220727114450_create_manufacturers.rb │ │ │ ├── 20220719094450_create_drivers.rb │ │ │ ├── 20190226172951_create_user.rb │ │ │ ├── 20210511141752_create_owners.rb │ │ │ ├── 20190226173051_create_isle.rb │ │ │ ├── 20220719094127_create_cars.rb │ │ │ ├── 20210326140855_create_locations.rb │ │ │ ├── 20220727114930_add_columns_to_products.rb │ │ │ ├── 20190226174951_create_tree.rb │ │ │ └── 20231117084236_create_addresses.rb │ ├── config.ru │ ├── lib │ │ └── forest_liana │ │ │ ├── collections │ │ │ ├── location.rb │ │ │ └── user.rb │ │ │ └── controllers │ │ │ ├── owners_controller.rb │ │ │ └── owner_trees_controller.rb │ ├── Rakefile │ └── README.rdoc ├── requests │ ├── test.ru │ └── cors_spec.rb ├── helpers │ └── forest_liana │ │ └── schema_helper_spec.rb ├── services │ └── forest_liana │ │ └── utils │ │ └── context_variables_spec.rb └── config │ └── initializers │ └── logger_spec.rb ├── config ├── initializers │ ├── arel-helpers.rb │ ├── time_formats.rb │ ├── httpclient.rb │ ├── logger.rb │ └── error-messages.rb └── routes │ └── actions.rb ├── .DS_Store ├── lib ├── forest_liana │ ├── version.rb │ ├── mixpanel_event.rb │ ├── base64_string_io.rb │ └── json_printer.rb ├── tasks │ ├── clear_oidc_data.rake │ ├── display_apimap.rake │ └── send_apimap.rake └── forest_liana.rb ├── .husky └── commit-msg ├── .rakeTasks ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── SECURITY.md ├── bin └── rails ├── .generators ├── commitlint.config.js ├── Rakefile ├── .gitignore ├── package.json ├── Gemfile ├── forest_liana.gemspec └── .releaserc.js /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require rails_helper 2 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Forest Rails Liana 2 | -------------------------------------------------------------------------------- /app/assets/images/forest_liana/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/reference.yml: -------------------------------------------------------------------------------- 1 | reference_1: 2 | id: 1 3 | -------------------------------------------------------------------------------- /config/initializers/arel-helpers.rb: -------------------------------------------------------------------------------- 1 | require 'arel-helpers' 2 | -------------------------------------------------------------------------------- /spec/dummy/app/models/town.rb: -------------------------------------------------------------------------------- 1 | class Town < Location 2 | end 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/forest-rails/main/.DS_Store -------------------------------------------------------------------------------- /lib/forest_liana/version.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | VERSION = "9.3.16" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/driver.rb: -------------------------------------------------------------------------------- 1 | class Driver < UserRecord 2 | has_one :car 3 | end -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/reference.rb: -------------------------------------------------------------------------------- 1 | class Reference < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/car.rb: -------------------------------------------------------------------------------- 1 | class Car < GarageRecord 2 | belongs_to :driver 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/reference.rb: -------------------------------------------------------------------------------- 1 | class Reference < SubApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/date_field.rb: -------------------------------------------------------------------------------- 1 | class DateField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/float_field.rb: -------------------------------------------------------------------------------- 1 | class FloatField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/string_field.rb: -------------------------------------------------------------------------------- 1 | class StringField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/boolean_field.rb: -------------------------------------------------------------------------------- 1 | class BooleanField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/decimal_field.rb: -------------------------------------------------------------------------------- 1 | class DecimalField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/models/integer_field.rb: -------------------------------------------------------------------------------- 1 | class IntegerField < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /config/initializers/time_formats.rb: -------------------------------------------------------------------------------- 1 | Time::DATE_FORMATS[:forest_datetime] = "%d/%m/%Y %H:%M:%S" 2 | -------------------------------------------------------------------------------- /test/dummy/app/models/owner.rb: -------------------------------------------------------------------------------- 1 | class Owner < ActiveRecord::Base 2 | has_many :trees 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < ActiveRecord::Base 2 | belongs_to :owner 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/location.rb: -------------------------------------------------------------------------------- 1 | class Location < ActiveRecord::Base 2 | belongs_to :island 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/manufacturer.rb: -------------------------------------------------------------------------------- 1 | class Manufacturer < ApplicationRecord 2 | has_many :products 3 | end 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module ApplicationHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | local: 3 | service: Disk 4 | root: <%= Rails.root.join("storage") %> 5 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount ForestLiana::Engine => "/forest" 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | local: 3 | service: Disk 4 | root: <%= Rails.root.join("storage") %> 5 | -------------------------------------------------------------------------------- /spec/dummy/app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount ForestLiana::Engine => "/forest" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/serialize_field.rb: -------------------------------------------------------------------------------- 1 | class SerializeField < ActiveRecord::Base 2 | serialize :field, Array 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /lib/forest_liana/mixpanel_event.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module ForestLiana 4 | class MixpanelEvent < OpenStruct 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/sub_application_record.rb: -------------------------------------------------------------------------------- 1 | class SubApplicationRecord < ApplicationRecord 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/owner.rb: -------------------------------------------------------------------------------- 1 | class Owner < ActiveRecord::Base 2 | has_many :trees 3 | 4 | default_scope { order('hired_at ASC') } 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/models/polymorphic_field.rb: -------------------------------------------------------------------------------- 1 | class PolymorphicField < ActiveRecord::Base 2 | belongs_to :has_one_field, polymorphic: true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/has_many_class_name_field.rb: -------------------------------------------------------------------------------- 1 | class HasManyClassNameField < ActiveRecord::Base 2 | has_many :foo, class_name: 'BelongsToField' 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/belongs_to_class_name_field.rb: -------------------------------------------------------------------------------- 1 | class BelongsToClassNameField < ActiveRecord::Base 2 | belongs_to :foo, class_name: 'HasOneField' 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/has_and_belongs_to_many_field.rb: -------------------------------------------------------------------------------- 1 | class HasAndBelongsToManyField < ActiveRecord::Base 2 | has_and_belongs_to_many :has_many_field 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/has_many_field.rb: -------------------------------------------------------------------------------- 1 | class HasManyField < ActiveRecord::Base 2 | has_many :belongs_to_fields 3 | belongs_to :has_many_through_field 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ActiveRecord::Base 2 | self.table_name = 'addresses' 3 | 4 | belongs_to :addressable, polymorphic: true 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user_record.rb: -------------------------------------------------------------------------------- 1 | class UserRecord < ApplicationRecord 2 | self.abstract_class = true 3 | connects_to database: { writing: :user, reading: :user } 4 | end -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb: -------------------------------------------------------------------------------- 1 | class AddAgeToTree < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :trees, :age, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/garage_record.rb: -------------------------------------------------------------------------------- 1 | class GarageRecord < ApplicationRecord 2 | self.abstract_class = true 3 | connects_to database: { writing: :garage, reading: :garage } 4 | end -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190716135241_add_type_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddTypeToUser < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :users, :title, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/forest_liana.rb: -------------------------------------------------------------------------------- 1 | ForestLiana.env_secret = 'env_secret_test' 2 | ForestLiana.auth_secret = 'auth_secret_test' 3 | ForestLiana.application_url = 'http://localhost:3000' 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/has_many_through_field.rb: -------------------------------------------------------------------------------- 1 | class HasManyThroughField < ActiveRecord::Base 2 | has_many :belongs_to_fields, through: :has_many_fields 3 | has_many :has_many_fields 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/apimaps_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ApimapsController < ForestLiana::BaseController 3 | def index 4 | head :no_content 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/models/belongs_to_field.rb: -------------------------------------------------------------------------------- 1 | class BelongsToField < ActiveRecord::Base 2 | belongs_to :has_one_field 3 | belongs_to :has_many_field 4 | belongs_to :has_many_class_name_field 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160627172810_create_owner.rb: -------------------------------------------------------------------------------- 1 | class CreateOwner < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :owners do |t| 4 | t.string :name 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20181111162121_create_references_table.rb: -------------------------------------------------------------------------------- 1 | class CreateReferencesTable < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :references do |t| 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/clear_oidc_data.rake: -------------------------------------------------------------------------------- 1 | namespace :forest do 2 | desc "Clear the OIDC data cache key" 3 | task clear: :environment do 4 | Rails.cache.delete("#{ForestLiana.env_secret}-client-data") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210526084712_create_products.rb: -------------------------------------------------------------------------------- 1 | class CreateProducts < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :products do |t| 4 | t.string :uri 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608130516_create_date_field.rb: -------------------------------------------------------------------------------- 1 | class CreateDateField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :date_fields do |t| 4 | t.date :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210326110524_create_references.rb: -------------------------------------------------------------------------------- 1 | class CreateReferences < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :references do |t| 4 | 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608131610_create_float_field.rb: -------------------------------------------------------------------------------- 1 | class CreateFloatField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :float_fields do |t| 4 | t.float :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20220727114450_create_manufacturers.rb: -------------------------------------------------------------------------------- 1 | class CreateManufacturers < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :manufacturers do |t| 4 | t.string :name 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608131430_create_integer_field.rb: -------------------------------------------------------------------------------- 1 | class CreateIntegerField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :integer_fields do |t| 4 | t.integer :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608131603_create_decimal_field.rb: -------------------------------------------------------------------------------- 1 | class CreateDecimalField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :decimal_fields do |t| 4 | t.decimal :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608132159_create_boolean_field.rb: -------------------------------------------------------------------------------- 1 | class CreateBooleanField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :boolean_fields do |t| 4 | t.boolean :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608132621_create_string_field.rb: -------------------------------------------------------------------------------- 1 | class CreateStringField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :string_fields do |t| 4 | t.string :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150814081918_create_has_many_through_field.rb: -------------------------------------------------------------------------------- 1 | class CreateHasManyThroughField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :has_many_through_fields do |t| 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ApplicationRecord 2 | belongs_to :manufacturer 3 | belongs_to :driver, optional: true 4 | 5 | validates :uri, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp } 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20220719094450_create_drivers.rb: -------------------------------------------------------------------------------- 1 | class CreateDrivers < ActiveRecord::Migration[6.0] 2 | def change 3 | Driver.connection.create_table :drivers do |t| 4 | t.string :firstname 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20170614141921_create_serialize_field.rb: -------------------------------------------------------------------------------- 1 | class CreateSerializeField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :serialize_fields do |t| 4 | t.string :field 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190226172951_create_user.rb: -------------------------------------------------------------------------------- 1 | class CreateUser < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210511141752_create_owners.rb: -------------------------------------------------------------------------------- 1 | class CreateOwners < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :owners do |t| 4 | t.string :name 5 | t.datetime :hired_at 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/app/models/has_one_field.rb: -------------------------------------------------------------------------------- 1 | class HasOneField < ActiveRecord::Base 2 | enum status: [:submitted, :pending, :rejected] 3 | 4 | has_one :belongs_to_field 5 | has_one :belongs_to_class_name_field, foreign_key: :foo_id 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150623115554_create_has_many_class_name_field.rb: -------------------------------------------------------------------------------- 1 | class CreateHasManyClassNameField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :has_many_class_name_fields do |t| 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/island.rb: -------------------------------------------------------------------------------- 1 | class Island < ActiveRecord::Base 2 | self.table_name = 'isle' 3 | 4 | has_many :trees 5 | has_one :location 6 | has_one :eponymous_tree, ->(record) { where(name: record.name) }, class_name: 'Tree' 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160627172951_create_tree.rb: -------------------------------------------------------------------------------- 1 | class CreateTree < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :trees do |t| 4 | t.string :name 5 | t.references :owner, index: true 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150612112520_create_has_and_belongs_to_many_field.rb: -------------------------------------------------------------------------------- 1 | class CreateHasAndBelongsToManyField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :has_and_belongs_to_many_fields do |t| 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/lib/forest_liana/collections/location.rb: -------------------------------------------------------------------------------- 1 | class Forest::Location 2 | include ForestLiana::Collection 3 | 4 | collection :Location 5 | 6 | field :alter_coordinates, type: 'String' do 7 | object.name + 'XYZ' 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190226173051_create_isle.rb: -------------------------------------------------------------------------------- 1 | class CreateIsle < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :isle do |t| 4 | t.string :name 5 | t.binary :map 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20220719094127_create_cars.rb: -------------------------------------------------------------------------------- 1 | class CreateCars < ActiveRecord::Migration[6.0] 2 | def change 3 | Car.connection.create_table :cars do |t| 4 | t.string :model 5 | t.references :driver, index: true 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608150016_create_has_many_field.rb: -------------------------------------------------------------------------------- 1 | class CreateHasManyField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :has_many_fields do |t| 4 | t.references :has_many_through_field, index: true 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ::ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ::ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150616150629_create_polymorphic_field.rb: -------------------------------------------------------------------------------- 1 | class CreatePolymorphicField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :polymorphic_fields do |t| 4 | t.references :has_one_field, index: true 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608133044_create_has_one_field.rb: -------------------------------------------------------------------------------- 1 | class CreateHasOneField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :has_one_fields do |t| 4 | t.boolean :checked 5 | t.column :status, :integer, default: 0 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/display_apimap.rake: -------------------------------------------------------------------------------- 1 | namespace :forest do 2 | desc "Display the current Forest Apimap version" 3 | task(:display_apimap).clear 4 | task display_apimap: :environment do 5 | bootstrapper = ForestLiana::Bootstrapper.new 6 | bootstrapper.display_apimap 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150609114636_create_belongs_to_class_name_field.rb: -------------------------------------------------------------------------------- 1 | class CreateBelongsToClassNameField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :belongs_to_class_name_fields do |t| 4 | t.references :foo, index: true 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/services/forest_liana/objective_stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ObjectiveStatGetter < ValueStatGetter 3 | attr_accessor :objective 4 | 5 | def perform 6 | super 7 | @record.value = { value: @record.value[:countCurrent] } 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :trees_owned, class_name: 'Tree', inverse_of: :owner 3 | has_many :trees_cut, class_name: 'Tree', inverse_of: :cutter 4 | has_many :addresses, as: :addressable 5 | 6 | enum title: [ :king, :villager, :outlaw ] 7 | end 8 | -------------------------------------------------------------------------------- /spec/requests/test.ru: -------------------------------------------------------------------------------- 1 | require 'rack/cors' 2 | 3 | use Rack::Lint 4 | use Rack::Cors do 5 | allow do 6 | origins 'localhost:3000', 7 | '127.0.0.1:3000' 8 | 9 | resource '/', headers: :any, methods: :any 10 | resource '/options', methods: :options 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/forest/islands_controller.rb: -------------------------------------------------------------------------------- 1 | class Forest::IslandsController < ForestLiana::SmartActionsController 2 | def test 3 | render json: { success: 'You are OK.' } 4 | end 5 | 6 | def unknown_action 7 | render json: { success: 'unknown action' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210326140855_create_locations.rb: -------------------------------------------------------------------------------- 1 | class CreateLocations < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :locations do |t| 4 | t.string :coordinates 5 | t.references :island, index: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/lib/forest_liana/controllers/owners_controller.rb: -------------------------------------------------------------------------------- 1 | class Forest::OwnersController < ForestLiana::ResourcesController 2 | def count 3 | if (params[:search]) 4 | deactivate_count_response 5 | else 6 | params[:collection] = 'Owner' 7 | super 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20220727114930_add_columns_to_products.rb: -------------------------------------------------------------------------------- 1 | class AddColumnsToProducts < ActiveRecord::Migration[6.0] 2 | def change 3 | change_table :products do |table| 4 | table.string :name 5 | table.belongs_to(:manufacturer) 6 | table.belongs_to(:driver) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160628173505_add_timestamps.rb: -------------------------------------------------------------------------------- 1 | class AddTimestamps < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :owners, :created_at, :datetime 4 | add_column :owners, :updated_at, :datetime 5 | add_column :trees, :created_at, :datetime 6 | add_column :trees, :updated_at, :datetime 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/serialize_field.yml: -------------------------------------------------------------------------------- 1 | serialize_field_1: 2 | id: 1 3 | field: value 1 4 | 5 | serialize_field_2: 6 | id: 2 7 | field: value 2 8 | 9 | serialize_field_3: 10 | id: 3 11 | field: value 3 12 | 13 | serialize_field_4: 14 | id: 4 15 | field: value 4 16 | 17 | serialize_field_5: 18 | id: 5 19 | field: value 5 20 | -------------------------------------------------------------------------------- /.rakeTasks: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/views/layouts/forest_liana/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ForestLiana 5 | <%= stylesheet_link_tag "forest/application", media: "all" %> 6 | <%= javascript_include_tag "forest/application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190226174951_create_tree.rb: -------------------------------------------------------------------------------- 1 | class CreateTree < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :trees do |t| 4 | t.string :name 5 | t.references :owner, index: true 6 | t.references :cutter, index: true 7 | t.references :island, index: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/lib/forest_liana/controllers/owner_trees_controller.rb: -------------------------------------------------------------------------------- 1 | class Forest::OwnerTreesController < ForestLiana::AssociationsController 2 | def count 3 | if (params[:search]) 4 | deactivate_count_response 5 | else 6 | params[:collection] = 'Owner' 7 | params[:association_name] = 'trees' 8 | super 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20231117084236_create_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateAddresses < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :addresses do |t| 4 | t.string :line1 5 | t.string :city 6 | t.string :zipcode 7 | t.references :addressable, polymorphic: true, null: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/httpclient.rb: -------------------------------------------------------------------------------- 1 | require 'httpclient' 2 | 3 | class HTTPClient 4 | alias original_initialize initialize 5 | 6 | def initialize(*args, &block) 7 | original_initialize(*args, &block) 8 | # NOTICE: Force use of the default system CA certs (instead of the 6 year old bundled ones). 9 | @session_manager&.ssl_config&.set_default_paths 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150608133038_create_belongs_to_field.rb: -------------------------------------------------------------------------------- 1 | class CreateBelongsToField < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :belongs_to_fields do |t| 4 | t.references :has_one_field, index: true 5 | t.references :has_many_class_name_field, index: true 6 | t.references :has_many_field, index: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | namespace :forest do 3 | post '/actions/test' => 'islands#test' 4 | post '/actions/unknown_action' => 'islands#unknown_action' 5 | get '/Owner/count' , to: 'owners#count' 6 | get '/Owner/:id/relationships/trees/count' , to: 'owner_trees#count' 7 | end 8 | 9 | mount ForestLiana::Engine => "/forest" 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/models/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < ActiveRecord::Base 2 | belongs_to :owner, class_name: 'User', inverse_of: :trees_owned 3 | belongs_to :cutter, class_name: 'User', inverse_of: :trees_cut 4 | belongs_to :island 5 | belongs_to :eponymous_island, 6 | ->(record) { where(name: record.name) }, 7 | class_name: 'Island', 8 | inverse_of: :eponymous_tree, 9 | optional: true 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/lib/forest_liana/collections/user.rb: -------------------------------------------------------------------------------- 1 | class Forest::User 2 | include ForestLiana::Collection 3 | 4 | collection :User 5 | 6 | filter_cap_name = lambda do |condition, where| 7 | capitalize_name = condition['value'].capitalize 8 | "name IS '#{capitalize_name}'" 9 | end 10 | 11 | field :cap_name, type: 'String', filter: filter_cap_name do 12 | object.name.upcase 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_base_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeBaseGetter < IntegrationBaseGetter 3 | private 4 | 5 | def field 6 | ForestLiana.integrations[:stripe][:mapping].select { |value| 7 | value.split('.')[0] == ForestLiana::SchemaUtils 8 | .find_model_from_collection_name(@params[:collection]).try(:name) 9 | }.first.split('.')[1] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Definition of Done 2 | 3 | ### General 4 | 5 | - [ ] Write an explicit title for the Pull Request, following [Conventional Commits specification](https://www.conventionalcommits.org) 6 | - [ ] Test manually the implemented changes 7 | - [ ] Validate the code quality (indentation, syntax, style, simplicity, readability) 8 | 9 | ### Security 10 | 11 | - [ ] Consider the security impact of the changes made 12 | -------------------------------------------------------------------------------- /lib/tasks/send_apimap.rake: -------------------------------------------------------------------------------- 1 | namespace :forest do 2 | desc "Synchronize the models/customization with Forest servers" 3 | task(:send_apimap).clear 4 | task send_apimap: :environment do 5 | if ForestLiana.env_secret 6 | bootstrapper = ForestLiana::Bootstrapper.new(true) 7 | bootstrapper.synchronize(true) 8 | else 9 | puts 'Cannot send the Apimap, Forest cannot find your env_secret' 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a security vulnerability, please use the [Forest Admin security email](mailto:security@forestadmin.com). 6 | 7 | Our technical team will consider your request carefully. 8 | 9 | If the vulnerability report is accepted, Forest Admin will: 10 | - work on a fix of the current version with the highest priority, 11 | - let you know as soon as a new patched version is published. 12 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/exceptions/access_denied.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Exceptions 4 | class AccessDenied < ForestLiana::Errors::ExpectedError 5 | def initialize 6 | super( 7 | 403, 8 | :forbidden, 9 | 'You don\'t have permission to access this resource', 10 | 'AccessDenied' 11 | ) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/fetch.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Fetch 4 | def get_permissions(route) 5 | response = ForestLiana::ForestApiRequester.get(route) 6 | 7 | if response.is_a?(Net::HTTPOK) 8 | JSON.parse(response.body) 9 | else 10 | raise ForestLiana::Errors::HTTP403Error.new("Permission could not be retrieved") 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected behavior 2 | 3 | TODO: Please describe here the behavior you are expecting. 4 | 5 | ## Actual behavior 6 | 7 | TODO: What is the current behavior? 8 | 9 | ## Failure Logs 10 | 11 | TODO: Please include any relevant log snippets, if necessary. 12 | 13 | ## Context 14 | 15 | TODO: Please provide any relevant information about your setup. 16 | 17 | * Package Version: 18 | * Rails Version: 19 | * Database Dialect: 20 | * Database Version: 21 | -------------------------------------------------------------------------------- /app/services/forest_liana/oidc_configuration_retriever.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class OidcConfigurationRetriever 3 | def self.retrieve() 4 | response = ForestLiana::ForestApiRequester.get('/oidc/.well-known/openid-configuration') 5 | if response.is_a?(Net::HTTPOK) 6 | return JSON.parse(response.body) 7 | else 8 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:OIDC_CONFIGURATION_RETRIEVAL_FAILED] 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/exceptions/unknown_collection.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Exceptions 4 | class UnknownCollection < ForestLiana::Errors::ExpectedError 5 | def initialize(collection_name) 6 | super( 7 | 409, 8 | :conflict, 9 | "The collection #{collection_name} doesn't exist", 10 | 'collection not found' 11 | ) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/forest_liana/model/stat.rb: -------------------------------------------------------------------------------- 1 | class ForestLiana::Model::Stat 2 | include ActiveModel::Validations 3 | include ActiveModel::Conversion 4 | include ActiveModel::Serialization 5 | extend ActiveModel::Naming 6 | 7 | attr_accessor :value 8 | 9 | def initialize(attributes = {}) 10 | attributes.each do |name, value| 11 | send("#{name}=", value) 12 | end 13 | end 14 | 15 | def persisted? 16 | false 17 | end 18 | 19 | def id 20 | SecureRandom.uuid 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/exceptions/trigger_forbidden.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Exceptions 4 | class TriggerForbidden < ForestLiana::Errors::ExpectedError 5 | def initialize 6 | super( 7 | 403, 8 | :forbidden, 9 | 'You don\'t have the permission to trigger this action', 10 | 'CustomActionTriggerForbiddenError' 11 | ) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_payment_refunder.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripePaymentRefunder 3 | def initialize(params) 4 | @params = params 5 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 6 | end 7 | 8 | def perform 9 | return unless @params[:data][:attributes][:ids] 10 | 11 | @params[:data][:attributes][:ids].each do |id| 12 | ch = ::Stripe::Charge.retrieve(id) 13 | ch.refunds.create 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/has_many_through_field.yml: -------------------------------------------------------------------------------- 1 | has_many_through_field_1: 2 | id: 1 3 | 4 | has_many_through_field_2: 5 | id: 2 6 | 7 | has_many_through_field_3: 8 | id: 3 9 | 10 | has_many_through_field_4: 11 | id: 4 12 | 13 | has_many_through_field_5: 14 | id: 5 15 | 16 | has_many_through_field_6: 17 | id: 6 18 | 19 | has_many_through_field_7: 20 | id: 7 21 | 22 | has_many_through_field_8: 23 | id: 8 24 | 25 | has_many_through_field_9: 26 | id: 9 27 | 28 | has_many_through_field_10: 29 | id: 10 30 | -------------------------------------------------------------------------------- /test/fixtures/owner.yml: -------------------------------------------------------------------------------- 1 | owner_1: 2 | id: 1 3 | name: Sandro Munda 4 | created_at: <%= Time.now.year - 1 %>-05-30 09:00:00 5 | updated_at: <%= Time.now.year %>-06-27 20:00:00 6 | 7 | owner_2: 8 | id: 2 9 | name: Arnaud Besnier 10 | created_at: <%= Time.now.year %>-05-02 09:00:00 11 | updated_at: <%= Time.now.year %>-06-28 08:00:00 12 | 13 | owner_3: 14 | id: 3 15 | name: John Doe 16 | created_at: <%= Time.now.year - 2 %>-05-02 09:00:00 17 | updated_at: <%= Time.now.year - 1 %>-06-28 08:00:00 18 | -------------------------------------------------------------------------------- /app/models/forest_liana/model/segment.rb: -------------------------------------------------------------------------------- 1 | class ForestLiana::Model::Segment 2 | include ActiveModel::Validations 3 | include ActiveModel::Conversion 4 | include ActiveModel::Serialization 5 | extend ActiveModel::Naming 6 | 7 | attr_accessor :id, :name, :scope, :where 8 | 9 | def initialize(attributes = {}, &block) 10 | attributes.each do |name, value| 11 | send("#{name}=", value) 12 | end 13 | 14 | @where = block if block 15 | end 16 | 17 | def persisted? 18 | false 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/helpers/forest_liana/schema_helper_spec.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | describe SchemaHelper do 3 | describe '#find_collection_from_model' do 4 | context 'on a simple model' do 5 | it 'should return the schema collection related to the model' do 6 | collection = SchemaHelper.find_collection_from_model(User) 7 | expect(collection.class).to eq(ForestLiana::Model::Collection) 8 | expect(collection.name).to eq('User') 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/fixtures/has_many_field.yml: -------------------------------------------------------------------------------- 1 | has_many_field_1: 2 | id: 1 3 | 4 | has_many_field_2: 5 | id: 2 6 | 7 | has_many_field_3: 8 | id: 3 9 | 10 | has_many_field_4: 11 | id: 4 12 | 13 | has_many_field_5: 14 | id: 5 15 | has_many_through_field_id: 3 16 | 17 | has_many_field_6: 18 | id: 6 19 | has_many_through_field_id: 2 20 | 21 | has_many_field_7: 22 | id: 7 23 | 24 | has_many_field_8: 25 | id: 8 26 | has_many_through_field_id: 2 27 | 28 | has_many_field_9: 29 | id: 9 30 | 31 | has_many_field_10: 32 | id: 10 33 | -------------------------------------------------------------------------------- /app/services/forest_liana/integration_base_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntegrationBaseGetter 3 | private 4 | 5 | def pagination? 6 | @params[:page] && @params[:page][:number] 7 | end 8 | 9 | def limit 10 | return 10 unless pagination? 11 | 12 | if @params[:page][:size] 13 | @params[:page][:size].to_i 14 | else 15 | 10 16 | end 17 | end 18 | 19 | def collection 20 | @params[:collection].singularize.camelize.constantize 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/forest_liana/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/exceptions/action_condition_error.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Exceptions 4 | class ActionConditionError < ForestLiana::Errors::ExpectedError 5 | def initialize 6 | super( 7 | 409, 8 | :conflict, 9 | 'The conditions to trigger this action cannot be verified. Please contact an administrator.', 10 | 'InvalidActionConditionError' 11 | ) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/forest_liana/utils/beta_schema_utils.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Utils 3 | class BetaSchemaUtils 4 | def self.find_action_from_endpoint(collection_name, endpoint, http_method) 5 | collection = ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name } 6 | 7 | return nil unless collection 8 | 9 | collection.actions.find { |action| (action.endpoint == endpoint || "/#{action.endpoint}" == endpoint) && action.http_method == http_method } 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/exceptions/require_approval.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | module Exceptions 4 | class RequireApproval < ForestLiana::Errors::ExpectedError 5 | attr_reader :data 6 | def initialize(data) 7 | @data = data 8 | super( 9 | 403, 10 | :forbidden, 11 | 'This action requires to be approved.', 12 | 'CustomActionRequiresApprovalError', 13 | ) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/mixpanel_event_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class MixpanelEventSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :id 6 | attribute :event 7 | attribute :city 8 | attribute :region 9 | attribute :timezone 10 | attribute :os 11 | attribute :osVersion 12 | attribute :country 13 | attribute :date 14 | attribute :browser 15 | 16 | def self_link 17 | nil 18 | end 19 | 20 | def type 21 | @options[:context][:type] || 'mixpanel_events' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/routes/actions.rb: -------------------------------------------------------------------------------- 1 | ForestLiana.apimap.each do |collection| 2 | if !collection.actions.empty? 3 | collection.actions.each do |action| 4 | if action.hooks && action.hooks[:load].present? 5 | post action.endpoint.sub('forest', '') + '/hooks/load' => 'actions#load', action_name: ActiveSupport::Inflector.parameterize(action.name) 6 | end 7 | if action.hooks && action.hooks[:change].present? 8 | post action.endpoint.sub('forest', '') + '/hooks/change' => 'actions#change', action_name: ActiveSupport::Inflector.parameterize(action.name) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/schema_helper.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module SchemaHelper 3 | def self.find_collection_from_model(active_record_class) 4 | collection_name = ForestLiana.name_for(active_record_class) 5 | ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name } 6 | end 7 | 8 | def self.is_smart_field?(model, field_name) 9 | collection = self.find_collection_from_model(model) 10 | field_found = collection.fields.find { |collection_field| collection_field[:field].to_s == field_name } if collection 11 | field_found && field_found[:is_virtual] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.generators: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stat_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StatSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :value 6 | 7 | def type 8 | 'stats' 9 | end 10 | 11 | def format_name(attribute_name) 12 | attribute_name.to_s 13 | end 14 | 15 | def unformat_name(attribute_name) 16 | attribute_name.to_s.underscore 17 | end 18 | 19 | def self_link 20 | nil 21 | end 22 | 23 | def relationship_self_link(attribute_name) 24 | nil 25 | end 26 | 27 | def relationship_related_link(attribute_name) 28 | nil 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/services/forest_liana/has_many_associator.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class HasManyAssociator 3 | def initialize(resource, association, params) 4 | @resource = resource 5 | @association = association 6 | @params = params 7 | @data = params['data'] 8 | end 9 | 10 | def perform 11 | @record = @resource.find(@params[:id]) 12 | associated_records = @resource.find(@params[:id]).send(@association.name) 13 | 14 | if @data.is_a?(Array) 15 | @data.each do |record_added| 16 | associated_records << @association.klass.find(record_added[:id]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/forest_liana_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ForestLianaTest < ActiveSupport::TestCase 4 | test "truth" do 5 | assert_kind_of Module, ForestLiana 6 | end 7 | 8 | test 'config_dirs with no value set' do 9 | assert_equal( 10 | Rails.root.join('lib/forest_liana/**/*.rb'), 11 | ForestLiana.config_dir 12 | ) 13 | end 14 | 15 | test 'config_dirs with a value set' do 16 | ForestLiana.config_dir = 'lib/custom/**/*.rb' 17 | 18 | assert_equal( 19 | Rails.root.join('lib/custom/**/*.rb'), 20 | ForestLiana.config_dir 21 | ) 22 | 23 | ForestLiana.config_dir = 'lib/forest_liana/**/*.rb' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability/permission/request_permission.rb: -------------------------------------------------------------------------------- 1 | require 'jwt' 2 | 3 | module ForestLiana 4 | module Ability 5 | module Permission 6 | class RequestPermission 7 | def self.decodeSignedApprovalRequest(params) 8 | if (params[:data][:attributes][:signed_approval_request]) 9 | decode_parameters = JWT.decode(params[:data][:attributes][:signed_approval_request], ForestLiana.env_secret, true, { algorithm: 'HS256' }).try(:first) 10 | 11 | ActionController::Parameters.new(decode_parameters) 12 | else 13 | params 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/forest_liana/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_subscription_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeSubscriptionGetter < StripeBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def perform 11 | query = {} 12 | @record = ::Stripe::Subscription.retrieve(@params[:subscription_id]) 13 | 14 | query[field] = @record.customer 15 | if collection 16 | @record.customer = collection.find_by(query) 17 | else 18 | @record.customer = nil 19 | end 20 | @record 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/services/forest_liana/stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StatGetter < BaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(resource, params, forest_user) 6 | @resource = resource 7 | @params = params 8 | @user = forest_user 9 | 10 | validate_params 11 | compute_includes 12 | end 13 | 14 | def validate_params 15 | if @params.key?(:aggregator) && !%w[count sum avg max min].include?(@params[:aggregator].downcase) 16 | raise ForestLiana::Errors::HTTP422Error.new('Invalid aggregate function') 17 | end 18 | end 19 | 20 | def get_resource 21 | super 22 | @resource.reorder('') 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/devise_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class DeviseController < ForestLiana::ApplicationController 3 | def change_password 4 | resource = SchemaUtils.find_model_from_collection_name( 5 | params['data']['attributes']['collection_name']) 6 | 7 | user = resource.find(params['data']['attributes']['ids'].first) 8 | user.password = params['data']['attributes']['values']['New password'] 9 | user.save 10 | 11 | if user.valid? 12 | head :no_content 13 | else 14 | render status: 400, json: { 15 | error: user.errors.try(:messages).try(:[], :password) 16 | } 17 | end 18 | end 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/services/forest_liana/resource_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ResourceGetter < BaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(resource, params, forest_user) 6 | @resource = resource 7 | @params = params 8 | @collection_name = ForestLiana.name_for(resource) 9 | @user = forest_user 10 | @collection = get_collection(@collection_name) 11 | compute_includes() 12 | end 13 | 14 | def perform 15 | records = optimize_record_loading(@resource, get_resource()) 16 | scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(records, @user, @collection_name, @params[:timezone]) 17 | @record = scoped_records.find(@params[:id]) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_source_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeSourceGetter < StripeBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def perform 11 | resource = collection.find(@params[:recordId]) 12 | customer = resource[field] 13 | 14 | @record = ::Stripe::Customer 15 | .retrieve({ id: customer, expand: ['sources'] }) 16 | .sources.retrieve(@params[:objectId]) 17 | 18 | query = {} 19 | query[field] = @record.customer 20 | @record.customer = collection.find_by(query) 21 | 22 | @record 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/forest_liana/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_payment_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripePaymentGetter < StripeBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def perform 11 | query = {} 12 | @record = ::Stripe::Charge.retrieve(@params[:payment_id]) 13 | 14 | @record.created = Time.at(@record.created).to_datetime 15 | @record.amount /= 100.00 16 | 17 | query[field] = @record.customer 18 | if collection 19 | @record.customer = collection.find_by(query) 20 | else 21 | @record.customer = nil 22 | end 23 | @record 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | garage: 24 | <<: *default 25 | database: db/test2.sqlite3 26 | user: 27 | <<: *default 28 | database: db/test3.sqlite3 29 | 30 | production: 31 | <<: *default 32 | database: db/production.sqlite3 33 | -------------------------------------------------------------------------------- /lib/forest_liana/base64_string_io.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class Base64StringIO < StringIO 3 | class ArgumentError < StandardError; end 4 | 5 | attr_accessor :file_format 6 | 7 | def initialize(encoded_file) 8 | description, encoded_bytes = encoded_file.split(",") 9 | 10 | raise ArgumentError unless encoded_bytes 11 | raise ArgumentError if encoded_bytes.eql?("(null)") 12 | 13 | @file_format = get_file_format description 14 | bytes = ::Base64.decode64 encoded_bytes 15 | 16 | super bytes 17 | end 18 | 19 | def original_filename 20 | File.basename("file.#{@file_format}") 21 | end 22 | 23 | private 24 | 25 | def get_file_format(description) 26 | regex = /([a-z0-9]+);base64\z/ 27 | regex.match(description).try(:[], 1) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/services/forest_liana/intercom_conversation_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomConversationGetter < IntegrationBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params) 6 | @params = params 7 | @access_token = ForestLiana.integrations[:intercom][:access_token] 8 | @intercom = ::Intercom::Client.new(token: @access_token) 9 | end 10 | 11 | def perform 12 | begin 13 | @record = @intercom.conversations.find(id: @params[:conversation_id]) 14 | rescue Intercom::ResourceNotFound 15 | @record = nil 16 | rescue Intercom::UnexpectedError => exception 17 | FOREST_REPORTER.report exception 18 | FOREST_LOGGER.error "Cannot retrieve the Intercom conversation: #{exception.message}" 19 | @record = nil 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // NOTICE: When a github "squash and merge" is performed, github add the PR link in the commit 2 | // message using the format ` (#)`. Github provide the target branch of the build, 3 | // so authorizing 4+5 = 9 characters more on main for the max header length should work 4 | // until we reach PR #99999. 5 | 6 | let maxLineLength = 100; 7 | 8 | const prExtrasChars = 9; 9 | 10 | const isPushEvent = process.env.GITHUB_EVENT_NAME === 'push'; 11 | 12 | if (isPushEvent) { 13 | maxLineLength += prExtrasChars; 14 | } 15 | 16 | module.exports = { 17 | extends: ['@commitlint/config-conventional'], 18 | rules: { 19 | 'header-max-length': [1, 'always', maxLineLength], 20 | 'body-max-line-length': [1, 'always', maxLineLength], 21 | 'footer-max-line-length': [1, 'always', maxLineLength], 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'ForestLiana' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | require 'rake/testtask' 28 | 29 | Rake::TestTask.new(:test) do |t| 30 | t.libs << 'lib' 31 | t.libs << 'test' 32 | t.pattern = 'test/**/*_test.rb' 33 | t.verbose = false 34 | end 35 | 36 | 37 | task default: :test 38 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/scopes_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ScopesController < ForestLiana::ApplicationController 3 | def invalidate_scope_cache 4 | begin 5 | rendering_id = params[:renderingId] 6 | 7 | unless rendering_id 8 | FOREST_LOGGER.error 'Missing renderingId' 9 | return render serializer: nil, json: { status: 400 }, status: :bad_request 10 | end 11 | 12 | ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id) 13 | return render serializer: nil, json: { status: 200 }, status: :ok 14 | rescue => error 15 | FOREST_REPORTER.report error 16 | FOREST_LOGGER.error "Error during scope cache invalidation: #{error.message}" 17 | render serializer: nil, json: {status: 500 }, status: :internal_server_error 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stripe_payment_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripePaymentSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :description 6 | attribute :refunded 7 | attribute :currency 8 | attribute :status 9 | attribute :amount 10 | attribute :created 11 | 12 | has_one :customer 13 | 14 | def self_link 15 | "/forest#{super}" 16 | end 17 | 18 | def type 19 | @options[:context][:type] || 'stripe_payments' 20 | end 21 | 22 | def format_name(attribute_name) 23 | attribute_name.to_s 24 | end 25 | 26 | def unformat_name(attribute_name) 27 | attribute_name.to_s 28 | end 29 | 30 | def relationship_self_link(attribute_name) 31 | nil 32 | end 33 | 34 | def relationship_related_link(attribute_name) 35 | nil 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)] 6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) 7 | require "rails/test_help" 8 | 9 | # Filter out Minitest backtrace while allowing backtrace from other libraries 10 | # to be shown. 11 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new 12 | 13 | # Load support files 14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 15 | 16 | # Load fixtures from the engine 17 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 18 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 19 | ActiveSupport::TestCase.fixtures :all 20 | end 21 | -------------------------------------------------------------------------------- /app/services/forest_liana/live_query_checker.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class LiveQueryChecker 3 | QUERY_SELECT = /\ASELECT\s.*FROM\s.*\z/im 4 | 5 | def initialize(query, context) 6 | @query = query.strip 7 | @context = context 8 | end 9 | 10 | def validate 11 | raise generate_error 'You cannot execute an empty SQL query.' if @query.blank? 12 | 13 | if @query.include?(';') && @query.index(';') < (@query.length - 1) 14 | raise generate_error 'You cannot chain SQL queries.' 15 | end 16 | 17 | raise generate_error 'Only SELECT queries are allowed.' if QUERY_SELECT.match(@query).nil? 18 | end 19 | 20 | private 21 | 22 | def generate_error message 23 | error_message = "#{@context}: #{message}" 24 | FOREST_LOGGER.error(error_message) 25 | ForestLiana::Errors::LiveQueryError.new(error_message) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /app/services/forest_liana/intercom_attributes_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomAttributesGetter < IntegrationBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params) 6 | @params = params 7 | @intercom = ::Intercom::Client.new(token: ForestLiana.integrations[:intercom][:access_token]) 8 | end 9 | 10 | def perform 11 | begin 12 | resource = collection.find(@params[:id]) 13 | user = @intercom.users.find(email: resource.email) 14 | 15 | user.segments = user.segments.map do |segment| 16 | @intercom.segments.find(id: segment.id) 17 | end 18 | @record = user 19 | rescue Intercom::ResourceNotFound 20 | rescue Intercom::UnexpectedError => exception 21 | FOREST_REPORTER.report exception 22 | FOREST_LOGGER.error "Cannot retrieve the Intercom attributes: #{exception.message}" 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/services/forest_liana/token.rb: -------------------------------------------------------------------------------- 1 | EXPIRATION_IN_SECONDS = 1.hours 2 | 3 | module ForestLiana 4 | class Token 5 | REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/; 6 | 7 | def self.expiration_in_days 8 | Time.current + EXPIRATION_IN_SECONDS 9 | end 10 | 11 | def self.expiration_in_seconds 12 | return Time.now.to_i + EXPIRATION_IN_SECONDS 13 | end 14 | 15 | def self.create_token(user, rendering_id) 16 | return JWT.encode({ 17 | id: user['id'], 18 | email: user['email'], 19 | first_name: user['first_name'], 20 | last_name: user['last_name'], 21 | team: user['teams'][0], 22 | role: user['role'], 23 | tags: user['tags'], 24 | rendering_id: rendering_id, 25 | exp: expiration_in_seconds(), 26 | permission_level: user['permission_level'], 27 | }, ForestLiana.auth_secret, 'HS256') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /test/dummy/log/ 11 | /test/dummy/db/*.sqlite3 12 | /spec/dummy/log/ 13 | /spec/dummy/db/*.sqlite3 14 | /spec/dummy/tmp/ 15 | /tmp/ 16 | .byebug_history 17 | /out 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | 24 | ## Documentation cache and generated files: 25 | /.yardoc/ 26 | /_yardoc/ 27 | /doc/ 28 | /rdoc/ 29 | 30 | ## Environment normalisation: 31 | /.bundle/ 32 | /vendor/bundle 33 | /lib/bundler/man/ 34 | 35 | # for a library or gem, you might want to ignore these files since the code is 36 | # intended to run in multiple environments; otherwise, check them in: 37 | # Gemfile.lock 38 | # .ruby-gemset 39 | 40 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 41 | .rvmrc 42 | 43 | node_modules/ 44 | 45 | # IDE 46 | /.idea/ 47 | 48 | # rbenv 49 | .ruby-version 50 | 51 | -------------------------------------------------------------------------------- /test/services/forest_liana/operator_date_interval_parser_test.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class OperatorDateIntervalParserTest < ActiveSupport::TestCase 3 | test 'OPERATOR_AFTER_X_HOURS_AGO and OPERATOR_BEFORE_X_HOURS_AGO should not take timezone into account' do 4 | # Setting a big timezone (GMT+10) on purpose, the timezone should not be applied on the result date 5 | operatorDateIntervalParser = OperatorDateIntervalParser.new('Australia/Sydney') 6 | 7 | result = operatorDateIntervalParser.get_date_filter(OperatorDateIntervalParser::OPERATOR_AFTER_X_HOURS_AGO, 2) 8 | hourComputed = result.split('> ')[1].tr('\'', '').to_datetime.hour 9 | assert hourComputed == Time.now.utc.hour - 2 10 | 11 | result = operatorDateIntervalParser.get_date_filter(OperatorDateIntervalParser::OPERATOR_BEFORE_X_HOURS_AGO, 2) 12 | hourComputed = result.split('< ')[1].tr('\'', '').to_datetime.hour 13 | assert hourComputed == Time.now.utc.hour - 2 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stripe_bank_account_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeBankAccountSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :account_holder_name 6 | attribute :account_holder_type 7 | attribute :bank_name 8 | attribute :country 9 | attribute :currency 10 | attribute :fingerprint 11 | attribute :last4 12 | attribute :routing_number 13 | attribute :status 14 | 15 | has_one :customer 16 | 17 | def self_link 18 | "/forest#{super}" 19 | end 20 | 21 | def type 22 | @options[:context][:type] || 'stripe_bank_accounts' 23 | end 24 | 25 | def format_name(attribute_name) 26 | attribute_name.to_s 27 | end 28 | 29 | def unformat_name(attribute_name) 30 | attribute_name.to_s 31 | end 32 | 33 | def relationship_self_link(attribute_name) 34 | nil 35 | end 36 | 37 | def relationship_related_link(attribute_name) 38 | nil 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_invoice_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeInvoiceGetter < StripeBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def perform 11 | query = {} 12 | @record = ::Stripe::Invoice.retrieve(@params[:invoice_id]) 13 | 14 | @record.due_date = Time.at(@record.due_date).to_datetime unless @record.due_date.nil? 15 | @record.period_start = Time.at(@record.period_start).to_datetime 16 | @record.period_end = Time.at(@record.period_end).to_datetime 17 | @record.subtotal /= 100.00 18 | @record.total /= 100.00 19 | @record.amount_due /= 100.00 20 | 21 | query[field] = @record.customer 22 | if collection 23 | @record.customer = collection.find_by(query) 24 | else 25 | @record.customer = nil 26 | end 27 | @record 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /config/initializers/logger.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | module ForestLiana 3 | class Logger 4 | class << self 5 | def log 6 | if ForestLiana.logger != nil 7 | logger = ForestLiana.logger 8 | else 9 | logger = ::Logger.new(STDOUT) 10 | logger_colors = { 11 | DEBUG: 34, 12 | WARN: 33, 13 | ERROR: 31, 14 | INFO: 37 15 | } 16 | 17 | logger.formatter = proc do |severity, datetime, progname, message| 18 | displayed_message = "[#{datetime.to_s}] Forest 🌳🌳🌳 " \ 19 | "#{message}\n" 20 | "\e[#{logger_colors[severity.to_sym]}m#{displayed_message}\033[0m" 21 | end 22 | logger 23 | end 24 | end 25 | end 26 | end 27 | 28 | class Reporter 29 | def self.report (error) 30 | ForestLiana.reporter.report error if ForestLiana.reporter 31 | end 32 | end 33 | end 34 | 35 | FOREST_LOGGER = ForestLiana::Logger.log 36 | FOREST_REPORTER = ForestLiana::Reporter 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forest-rails", 3 | "version": "9.3.16", 4 | "description": "The official Rails liana for Forest.", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "prepare": "husky install", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ForestAdmin/forest-rails.git" 15 | }, 16 | "author": "", 17 | "license": "GPL-3.0", 18 | "bugs": { 19 | "url": "https://github.com/ForestAdmin/forest-rails/issues" 20 | }, 21 | "homepage": "https://github.com/ForestAdmin/forest-rails#readme", 22 | "devDependencies": { 23 | "@commitlint/cli": "17.4.2", 24 | "@commitlint/config-conventional": "17.4.2", 25 | "@semantic-release/changelog": "6.0.1", 26 | "@semantic-release/exec": "6.0.3", 27 | "@semantic-release/git": "10.0.1", 28 | "husky": "7.0.0", 29 | "semantic-release": "19.0.5", 30 | "semantic-release-rubygem": "1.2.0", 31 | "semantic-release-slack-bot": "3.5.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 5e5f9973f109380c6588c49162a9f7eb4c55c52b701f9764f814d8d6cf950c1cca7af8623746f90b0150e30eabb1ef74d21993c4f851e5936e5b17c1e72a4e89 15 | 16 | test: 17 | secret_key_base: a19488172a9b027882008a059081c4e8d41c078880c8a48d34c9ba4a736f238dc865ab5a8f4289e20d264145d758ff1b4b0c1493c0e47a463dd511050058bb57 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /test/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 5e5f9973f109380c6588c49162a9f7eb4c55c52b701f9764f814d8d6cf950c1cca7af8623746f90b0150e30eabb1ef74d21993c4f851e5936e5b17c1e72a4e89 15 | 16 | test: 17 | secret_key_base: a19488172a9b027882008a059081c4e8d41c078880c8a48d34c9ba4a736f238dc865ab5a8f4289e20d264145d758ff1b4b0c1493c0e47a463dd511050058bb57 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/intercom_conversation_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomConversationSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :created_at 6 | attribute :updated_at 7 | attribute :open 8 | attribute :read 9 | 10 | attribute :subject do 11 | object.conversation_message.subject 12 | end 13 | 14 | attribute :body do 15 | object.conversation_message.body 16 | end 17 | 18 | attribute :assignee do 19 | object.assignee.try(:email) 20 | end 21 | 22 | def self_link 23 | "/forest#{super}" 24 | end 25 | 26 | def type 27 | @options[:context][:type] || 'intercom-conversations' 28 | end 29 | 30 | def format_name(attribute_name) 31 | attribute_name.to_s 32 | end 33 | 34 | def unformat_name(attribute_name) 35 | attribute_name.to_s.underscore 36 | end 37 | 38 | def relationship_self_link(attribute_name) 39 | nil 40 | end 41 | 42 | def relationship_related_link(attribute_name) 43 | nil 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stripe_card_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeCardSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :last4 6 | attribute :brand 7 | attribute :funding 8 | attribute :exp_month 9 | attribute :exp_year 10 | attribute :country 11 | attribute :name 12 | attribute :address_line1 13 | attribute :address_line2 14 | attribute :address_city 15 | attribute :address_state 16 | attribute :address_zip 17 | attribute :address_country 18 | attribute :cvc_check 19 | 20 | has_one :customer 21 | 22 | def self_link 23 | "/forest#{super}" 24 | end 25 | 26 | def type 27 | @options[:context][:type] || 'stripe_cards' 28 | end 29 | 30 | def format_name(attribute_name) 31 | attribute_name.to_s 32 | end 33 | 34 | def unformat_name(attribute_name) 35 | attribute_name.to_s 36 | end 37 | 38 | def relationship_self_link(attribute_name) 39 | nil 40 | end 41 | 42 | def relationship_related_link(attribute_name) 43 | nil 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stripe_subscription_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeSubscriptionSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :cancel_at_period_end 6 | attribute :canceled_at 7 | attribute :created 8 | attribute :current_period_end 9 | attribute :current_period_start 10 | attribute :ended_at 11 | attribute :livemode 12 | attribute :quantity 13 | attribute :start 14 | attribute :status 15 | attribute :tax_percent 16 | attribute :trial_end 17 | attribute :trial_start 18 | 19 | has_one :customer 20 | 21 | def self_link 22 | "/forest#{super}" 23 | end 24 | 25 | def type 26 | @options[:context][:type] || 'stripe_subscriptions' 27 | end 28 | 29 | def format_name(attribute_name) 30 | attribute_name.to_s 31 | end 32 | 33 | def unformat_name(attribute_name) 34 | attribute_name.to_s 35 | end 36 | 37 | def relationship_self_link(attribute_name) 38 | nil 39 | end 40 | 41 | def relationship_related_link(attribute_name) 42 | nil 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "forest_liana" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | 22 | # Do not swallow errors in after_commit/after_rollback callbacks. 23 | # config.active_record.raise_in_transactional_callbacks = true 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "forest_liana" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | 22 | # Do not swallow errors in after_commit/after_rollback callbacks. 23 | # config.active_record.raise_in_transactional_callbacks = true 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/decoration_helper.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module DecorationHelper 3 | def self.detect_match_and_decorate record, index, field_name, value, search_value, match_fields 4 | begin 5 | match = value.match(/#{search_value}/i) 6 | if match 7 | match_fields[index] = { id: record['id'], search: [] } if match_fields[index].nil? 8 | match_fields[index][:search] << field_name 9 | end 10 | rescue 11 | end 12 | end 13 | 14 | def self.decorate_for_search(records_serialized, field_names, search_value) 15 | match_fields = {} 16 | records_serialized['data'].each_with_index do |record, index| 17 | field_names.each do |field_name| 18 | value = record['attributes'][field_name] 19 | if value 20 | detect_match_and_decorate(record, index, field_name, value, search_value, match_fields) 21 | end 22 | end 23 | 24 | detect_match_and_decorate(record, index, 'id', record['id'], search_value, match_fields) 25 | end 26 | match_fields.empty? ? nil : match_fields 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/mixpanel_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class MixpanelController < ForestLiana::ApplicationController 3 | def last_events 4 | collection_name = params[:collection] 5 | mapping = ForestLiana.integrations[:mixpanel][:mapping] 6 | mapping_for_current_collection = mapping.find { |item| item.start_with?(collection_name) } 7 | field_name = mapping_for_current_collection.split('.')[1] 8 | id = params[:id] 9 | field_value = collection_name.constantize.find_by('id': id)[field_name] 10 | 11 | getter = ForestLiana::MixpanelLastEventsGetter.new(params) 12 | getter.perform(field_name, field_value) 13 | 14 | custom_properties = ForestLiana.integrations[:mixpanel][:custom_properties] 15 | MixpanelEventSerializer.attributes(*custom_properties) 16 | 17 | render serializer: nil, json: serialize_models(getter.records, { 18 | context: { type: get_serializer_type('mixpanel_events') }, 19 | count: getter.count, 20 | }) 21 | end 22 | 23 | def get_serializer_type(suffix) 24 | "#{params[:collection]}_#{suffix}" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in forest.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | group :development, :test do 15 | gem 'byebug' 16 | gem 'rspec-rails' 17 | gem "timecop" 18 | end 19 | 20 | group :test do 21 | gem 'rake' 22 | gem 'sqlite3', '~> 1.4' 23 | gem 'simplecov', '~> 0.17.0', require: false 24 | end 25 | 26 | gem 'rails', '6.1.7.8' 27 | gem 'forestadmin-jsonapi-serializers' 28 | gem 'rack-cors' 29 | gem 'arel-helpers', '2.14.0' 30 | gem 'groupdate', '5.2.2' 31 | gem 'useragent' 32 | gem 'jwt' 33 | gem 'bcrypt' 34 | gem 'httparty', '0.21.0' 35 | gem 'ipaddress', '0.8.3' 36 | gem 'openid_connect', '1.4.2' 37 | gem 'json' 38 | gem 'json-jwt', '>= 1.16' 39 | gem 'deepsort' 40 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/intercom_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomController < ForestLiana::ApplicationController 3 | def conversations 4 | getter = IntercomConversationsGetter.new(params) 5 | getter.perform 6 | 7 | render serializer: nil, json: serialize_models(getter.records, { 8 | context: { type: get_serializer_type('intercom_conversations') }, 9 | meta: { count: getter.count } 10 | }) 11 | end 12 | 13 | def conversation 14 | getter = IntercomConversationGetter.new(params) 15 | getter.perform 16 | 17 | render serializer: nil, json: serialize_model(getter.record, { 18 | context: { type: get_serializer_type('intercom_conversations') } 19 | }) 20 | end 21 | 22 | def attributes 23 | getter = IntercomAttributesGetter.new(params) 24 | getter.perform 25 | 26 | render serializer: nil, json: serialize_model(getter.record, { 27 | context: { type: get_serializer_type('intercom_attributes') } 28 | }) 29 | end 30 | 31 | def get_serializer_type(suffix) 32 | "#{params[:collection]}_#{suffix}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/stripe_invoice_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeInvoiceSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :amount_due 6 | attribute :amount_paid 7 | attribute :amount_remaining 8 | attribute :application_fee_amount 9 | attribute :attempt_count 10 | attribute :attempted 11 | attribute :currency 12 | attribute :due_date 13 | attribute :paid 14 | attribute :period_end 15 | attribute :period_start 16 | attribute :status 17 | attribute :subtotal 18 | attribute :total 19 | attribute :tax 20 | 21 | has_one :customer 22 | 23 | def self_link 24 | "/forest#{super}" 25 | end 26 | 27 | def type 28 | @options[:context][:type] || 'stripe_invoices' 29 | end 30 | 31 | def format_name(attribute_name) 32 | attribute_name.to_s 33 | end 34 | 35 | def unformat_name(attribute_name) 36 | attribute_name.to_s 37 | end 38 | 39 | def relationship_self_link(attribute_name) 40 | nil 41 | end 42 | 43 | def relationship_related_link(attribute_name) 44 | nil 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scaffold.css: -------------------------------------------------------------------------------- 1 | body { background-color: #fff; color: #333; } 2 | 3 | body, p, ol, ul, td { 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; 7 | } 8 | 9 | pre { 10 | background-color: #eee; 11 | padding: 10px; 12 | font-size: 11px; 13 | } 14 | 15 | a { color: #000; } 16 | a:visited { color: #666; } 17 | a:hover { color: #fff; background-color:#000; } 18 | 19 | div.field, div.actions { 20 | margin-bottom: 10px; 21 | } 22 | 23 | #notice { 24 | color: green; 25 | } 26 | 27 | .field_with_errors { 28 | padding: 2px; 29 | background-color: red; 30 | display: table; 31 | } 32 | 33 | #error_explanation { 34 | width: 450px; 35 | border: 2px solid red; 36 | padding: 7px; 37 | padding-bottom: 0; 38 | margin-bottom: 20px; 39 | background-color: #f0f0f0; 40 | } 41 | 42 | #error_explanation h2 { 43 | text-align: left; 44 | font-weight: bold; 45 | padding: 5px 5px 5px 15px; 46 | font-size: 12px; 47 | margin: -7px; 48 | margin-bottom: 0px; 49 | background-color: #c00; 50 | color: #fff; 51 | } 52 | 53 | #error_explanation ul li { 54 | font-size: 12px; 55 | list-style: square; 56 | } 57 | -------------------------------------------------------------------------------- /app/services/forest_liana/belongs_to_updater.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class BelongsToUpdater 3 | attr_accessor :errors 4 | 5 | def initialize(resource, association, params) 6 | @resource = resource 7 | @association = association 8 | @params = params 9 | @data = params['data'] 10 | @errors = nil 11 | end 12 | 13 | def perform 14 | begin 15 | @record = @resource.find(@params[:id]) 16 | if (SchemaUtils.polymorphic?(@association)) 17 | if @data.nil? 18 | new_value = nil 19 | else 20 | association_klass = SchemaUtils.polymorphic_models(@association).select { |a| a.name == @data[:type] }.first 21 | new_value = association_klass.find(@data[:id]) if @data && @data[:id] 22 | end 23 | else 24 | new_value = @association.klass.find(@data[:id]) if @data && @data[:id] 25 | end 26 | @record.send("#{@association.name}=", new_value) 27 | 28 | @record.save 29 | rescue ActiveRecord::SerializationTypeMismatch => exception 30 | @errors = [{ detail: exception.message }] 31 | rescue => exception 32 | @errors = [{ detail: exception.message }] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/services/forest_liana/controller_factory.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ControllerFactory 3 | 4 | def self.define_controller(active_record_class, service) 5 | controller_name = self.build_controller_name(active_record_class) 6 | controller_name_with_namespace = self.controller_name_with_namespace(controller_name) 7 | 8 | unless ForestLiana::UserSpace.const_defined?(controller_name_with_namespace) 9 | ForestLiana::UserSpace.const_set(controller_name, service) 10 | end 11 | end 12 | 13 | def self.get_controller_name(active_record_class) 14 | controller_name = self.build_controller_name(active_record_class) 15 | self.controller_name_with_namespace(controller_name) 16 | end 17 | 18 | def controller_for(active_record_class) 19 | controller = Class.new(ForestLiana::ResourcesController) { } 20 | 21 | ForestLiana::ControllerFactory.define_controller(active_record_class, controller) 22 | controller 23 | end 24 | 25 | private 26 | 27 | def self.controller_name_with_namespace(controller_name) 28 | "ForestLiana::UserSpace::#{controller_name}" 29 | end 30 | 31 | def self.build_controller_name(active_record_class) 32 | component_prefix = ForestLiana.component_prefix(active_record_class) 33 | "#{component_prefix}Controller" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/fixtures/tree.yml: -------------------------------------------------------------------------------- 1 | tree_1: 2 | id: 1 3 | name: Oak 4 | owner_id: 1 5 | created_at: <%= Time.now.year - 7 %>-02-11 11:00:00 6 | updated_at: <%= Time.now.year - 7 %>-02-11 11:00:00 7 | 8 | tree_2: 9 | id: 2 10 | name: Mapple 11 | owner_id: 2 12 | created_at: <%= Time.now.year - 7 %>-02-15 21:00:00 13 | updated_at: <%= Time.now.year - 7 %>-02-15 21:00:00 14 | 15 | tree_3: 16 | id: 3 17 | name: Mapple 18 | owner_id: 2 19 | created_at: <%= Time.now.year - 5 %>-04-11 12:00:00 20 | updated_at: <%= Time.now.year - 5 %>-04-11 12:00:00 21 | 22 | tree_4: 23 | id: 4 24 | name: Oak 25 | owner_id: 2 26 | created_at: <%= Time.now.year - 2 %>-06-18 09:00:00 27 | updated_at: <%= Time.now.year - 2 %>-06-18 09:00:00 28 | 29 | tree_5: 30 | id: 5 31 | name: Oak 32 | owner_id: 3 33 | created_at: <%= Time.now.year - 3 %>-06-18 09:00:00 34 | updated_at: <%= Time.now.year - 3 %>-06-18 09:00:00 35 | 36 | tree_6: 37 | id: 6 38 | name: Oak 39 | owner_id: 3 40 | created_at: <%= Time.now + 6.hours + 1.minute %> 41 | updated_at: <%= Time.now + 6.hours + 1.minute %> 42 | 43 | tree_7: 44 | id: 7 45 | name: Sequoia 46 | owner_id: 1 47 | created_at: <%= Time.now + 6.hours + 1.minute %> 48 | updated_at: <%= Time.now + 6.hours + 1.minute %> 49 | 50 | tree_8: 51 | id: 8 52 | name: Fir 53 | owner_id: 1 54 | created_at: <%= Time.now + 6.hours + 1.minute %> 55 | updated_at: <%= Time.now + 6.hours + 1.minute %> 56 | -------------------------------------------------------------------------------- /app/services/forest_liana/resource_updater.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ResourceUpdater 3 | attr_accessor :record 4 | attr_accessor :errors 5 | 6 | def initialize(resource, params, forest_user) 7 | @resource = resource 8 | @params = params 9 | @errors = nil 10 | @user = forest_user 11 | end 12 | 13 | def perform 14 | begin 15 | collection_name = ForestLiana.name_for(@resource) 16 | scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, @user, collection_name, @params[:timezone]) 17 | @record = scoped_records.find(@params[:id]) 18 | 19 | if has_strong_parameter 20 | @record.update(resource_params) 21 | else 22 | @record.update(resource_params, without_protection: true) 23 | end 24 | rescue ActiveRecord::StatementInvalid => exception 25 | # NOTICE: SQL request cannot be executed properly 26 | @errors = [{ detail: exception.cause.error }] 27 | rescue ForestLiana::Errors::SerializeAttributeBadFormat => exception 28 | @errors = [{ detail: exception.message }] 29 | rescue => exception 30 | @errors = [{ detail: exception.message }] 31 | end 32 | end 33 | 34 | def resource_params 35 | ResourceDeserializer.new(@resource, @params, false).perform 36 | end 37 | 38 | def has_strong_parameter 39 | Rails::VERSION::MAJOR > 5 || @resource.instance_method(:update!).arity == 1 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/services/forest_liana/utils/context_variables.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Utils 3 | class ContextVariables 4 | attr_reader :team, :user, :request_context_variables 5 | 6 | USER_VALUE_PREFIX = 'currentUser.'.freeze 7 | 8 | USER_VALUE_TAG_PREFIX = 'currentUser.tags.'.freeze 9 | 10 | USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze 11 | 12 | def initialize(team, user, request_context_variables = nil) 13 | @team = team 14 | @user = user 15 | @request_context_variables = request_context_variables 16 | end 17 | 18 | def get_value(context_variable_key) 19 | return get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_PREFIX) 20 | 21 | request_context_variables[context_variable_key] if request_context_variables 22 | end 23 | 24 | private 25 | 26 | def get_current_user_data(context_variable_key) 27 | if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX) 28 | return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..]] 29 | end 30 | 31 | if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX) 32 | user['tags'].each do |tag| 33 | return tag[context_variable_key[USER_VALUE_TAG_PREFIX.length..]] if tag.key?(context_variable_key[USER_VALUE_TAG_PREFIX.length..]) 34 | end 35 | end 36 | 37 | user[context_variable_key[USER_VALUE_PREFIX.length..]] 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/base_controller.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class BaseController < ::ActionController::Base 3 | skip_before_action :verify_authenticity_token, raise: false 4 | wrap_parameters false 5 | before_action :reject_unauthorized_ip 6 | 7 | def route_not_found 8 | head :not_found 9 | end 10 | 11 | private 12 | 13 | def reject_unauthorized_ip 14 | begin 15 | ip = request.remote_ip 16 | 17 | if !ForestLiana::IpWhitelist.is_ip_whitelist_retrieved || !ForestLiana::IpWhitelist.is_ip_valid(ip) 18 | unless ForestLiana::IpWhitelist.retrieve 19 | raise ForestLiana::Errors::HTTP403Error.new("IP whitelist not retrieved") 20 | end 21 | 22 | unless ForestLiana::IpWhitelist.is_ip_valid(ip) 23 | raise ForestLiana::Errors::HTTP403Error.new("IP address rejected (#{ip})") 24 | end 25 | end 26 | rescue ForestLiana::Errors::ExpectedError => exception 27 | error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{ 28 | status: exception.error_code, 29 | detail: exception.message 30 | }]) 31 | render(serializer: nil, json: error_data, status: exception.status) 32 | rescue => exception 33 | FOREST_REPORTER.report exception 34 | FOREST_LOGGER.error(exception) 35 | FOREST_LOGGER.error(exception.backtrace.join("\n")) 36 | render(serializer: nil, json: nil, status: :internal_server_error) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /forest_liana.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "forest_liana/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "forest_liana" 9 | s.version = ForestLiana::VERSION 10 | s.authors = ["Sandro Munda"] 11 | s.email = ["sandro@munda.me"] 12 | s.homepage = 'https://github.com/ForestAdmin/forest-rails' 13 | s.summary = "Official Rails Liana for Forest" 14 | s.description = "Forest is a modern admin interface that works on all major web frameworks. forest_liana is the gem that makes Forest admin work on any Rails application (Rails >= 4.0)." 15 | s.license = "GPL-3.0" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*", "spec/**/*"] 19 | 20 | s.add_runtime_dependency "rails", ">= 4.0" 21 | s.add_runtime_dependency "forestadmin-jsonapi-serializers", ">= 0.14.0" 22 | s.add_runtime_dependency "jwt" 23 | s.add_runtime_dependency "rack-cors" 24 | s.add_runtime_dependency "arel-helpers" 25 | s.add_runtime_dependency "groupdate", ">= 5.0.0" 26 | s.add_runtime_dependency "useragent" 27 | s.add_runtime_dependency "bcrypt" 28 | s.add_runtime_dependency "httparty" 29 | s.add_runtime_dependency "ipaddress" 30 | s.add_runtime_dependency "json" 31 | s.add_runtime_dependency "json-jwt", ">= 1.16.0" 32 | s.add_runtime_dependency "openid_connect", "1.4.2" 33 | s.add_runtime_dependency "deepsort" 34 | end 35 | -------------------------------------------------------------------------------- /app/services/forest_liana/forest_api_requester.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | module ForestLiana 4 | class ForestApiRequester 5 | def self.get(route, query: nil, headers: {}) 6 | begin 7 | HTTParty.get("#{forest_api_url}#{route}", { 8 | :verify => Rails.env.production?, 9 | headers: base_headers.merge(headers), 10 | query: query, 11 | }).response 12 | rescue 13 | raise "Cannot reach Forest API at #{forest_api_url}#{route}, it seems to be down right now." 14 | end 15 | end 16 | 17 | def self.post(route, body: nil, query: nil, headers: {}) 18 | begin 19 | if route.start_with?('https://') 20 | post_route = route 21 | else 22 | post_route = "#{forest_api_url}#{route}" 23 | end 24 | 25 | HTTParty.post(post_route, { 26 | :verify => Rails.env.production?, 27 | headers: base_headers.merge(headers), 28 | query: query, 29 | body: body.to_json, 30 | }).response 31 | rescue 32 | raise "Cannot reach Forest API at #{post_route}, it seems to be down right now." 33 | end 34 | end 35 | 36 | private 37 | 38 | def self.base_headers 39 | base_headers = { 40 | 'Content-Type' => 'application/json', 41 | } 42 | base_headers['forest-secret-key'] = ForestLiana.env_secret if !ForestLiana.env_secret.nil? 43 | return base_headers 44 | end 45 | 46 | def self.forest_api_url 47 | ENV['FOREST_URL'] || 'https://api.forestadmin.com' 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/services/forest_liana/utils/context_variables_spec.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Utils 3 | describe ContextVariables do 4 | let(:user) do 5 | { 6 | 'id' => '1', 7 | 'email' => 'jd@forestadmin.com', 8 | 'first_name' => 'John', 9 | 'last_name' => 'Doe', 10 | 'team' => 'Operations', 11 | 'role' => 'role-test', 12 | 'tags' => [{'key' => 'tag1', 'value' => 'value1' }, {'key' => 'tag2', 'value' => 'value2'}], 13 | 'rendering_id'=> '1' 14 | } 15 | end 16 | 17 | let(:team) do 18 | { 19 | 'id' => 1, 20 | 'name' => 'Operations' 21 | } 22 | end 23 | 24 | let(:request_context_variables) do 25 | { 26 | 'foo.id' => 100 27 | } 28 | end 29 | 30 | it 'returns the request context variable key when the key is not present into the user data' do 31 | context_variables = described_class.new(team, user, request_context_variables) 32 | expect(context_variables.get_value('foo.id')).to eq(100) 33 | end 34 | 35 | it 'returns the corresponding value from the key provided of the user data' do 36 | context_variables = described_class.new(team, user, request_context_variables) 37 | expect(context_variables.get_value('currentUser.first_name')).to eq('John') 38 | expect(context_variables.get_value('currentUser.tags.key')).to eq('tag1') 39 | expect(context_variables.get_value('currentUser.team.id')).to eq(1) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/services/forest_liana/has_many_dissociator.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class HasManyDissociator 3 | def initialize(resource, association, params, forest_user) 4 | @resource = resource 5 | @association = association 6 | @params = params 7 | @with_deletion = @params[:delete].to_s == 'true' 8 | @data = params['data'] 9 | @forest_user = forest_user 10 | end 11 | 12 | def perform 13 | @record = @resource.find(@params[:id]) 14 | associated_records = @resource.find(@params[:id]).send(@association.name) 15 | 16 | remove_association = !@with_deletion || @association.macro == :has_and_belongs_to_many 17 | if @data.is_a?(Array) 18 | record_ids = @data.map { |record| record[:id] } 19 | elsif @data.dig('attributes').present? 20 | record_ids = ForestLiana::ResourcesGetter.get_ids_from_request(@params, @forest_user) 21 | else 22 | record_ids = Array.new 23 | end 24 | 25 | if !record_ids.nil? && record_ids.any? 26 | if remove_association 27 | record_ids.each do |id| 28 | associated_records.delete(@association.klass.find(id)) 29 | end 30 | end 31 | 32 | if @with_deletion 33 | record_ids = record_ids.select { |record_id| @association.klass.exists?(record_id) } 34 | @resource.transaction do 35 | record_ids.each do |id| 36 | record = @association.klass.find(id) 37 | record.destroy! 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/services/forest_liana/authorization_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class AuthorizationGetter 3 | def self.authenticate(rendering_id, auth_data) 4 | begin 5 | route = "/liana/v2/renderings/#{rendering_id.to_s}/authorization" 6 | headers = { 'forest-token' => auth_data[:forest_token] } 7 | 8 | response = ForestLiana::ForestApiRequester 9 | .get(route, query: {}, headers: headers) 10 | 11 | if response.code.to_i == 200 12 | body = JSON.parse(response.body, :symbolize_names => false) 13 | user = body['data']['attributes'] 14 | user['id'] = body['data']['id'] 15 | user 16 | else 17 | raise generate_authentication_error response 18 | end 19 | end 20 | end 21 | 22 | private 23 | def self.generate_authentication_error(error) 24 | case error[:message] 25 | when ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_AND_RENDERINGID_INCONSISTENT] 26 | return ForestLiana::Errors::InconsistentSecretAndRenderingError.new() 27 | when ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_NOT_FOUND] 28 | return ForestLiana::Errors::SecretNotFoundError.new() 29 | else 30 | end 31 | 32 | serverError = error[:jse_cause][:response][:body][:errors][0] || nil 33 | 34 | if !serverError.nil? && serverError[:name] == ForestLiana::MESSAGES[:SERVER_TRANSACTION][:names][:TWO_FACTOR_AUTHENTICATION_REQUIRED] 35 | return ForestLiana::Errors::TwoFactorAuthenticationRequiredError.new() 36 | end 37 | 38 | return StandardError.new(error) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/services/forest_liana/ip_whitelist.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IpWhitelist 3 | @@use_ip_whitelist = true 4 | @@ip_whitelist_rules = nil 5 | 6 | def self.retrieve 7 | begin 8 | response = ForestLiana::ForestApiRequester.get('/liana/v1/ip-whitelist-rules') 9 | 10 | if response.is_a?(Net::HTTPOK) 11 | body = JSON.parse(response.body) 12 | ip_whitelist_data = body['data']['attributes'] 13 | 14 | @@use_ip_whitelist = ip_whitelist_data['use_ip_whitelist'] 15 | @@ip_whitelist_rules = ip_whitelist_data['rules'] 16 | true 17 | else 18 | FOREST_LOGGER.error 'An error occured while retrieving your IP whitelist. Your Forest ' + 19 | 'env_secret seems to be missing or unknown. Can you check that you properly set your ' + 20 | 'Forest env_secret in the forest_liana initializer?' 21 | false 22 | end 23 | rescue => exception 24 | FOREST_REPORTER.report exception 25 | FOREST_LOGGER.error 'Cannot retrieve the IP Whitelist from the Forest server.' 26 | FOREST_LOGGER.error 'Which was caused by:' 27 | ForestLiana::Errors::ExceptionHelper.recursively_print(exception, margin: ' ', is_error: true) 28 | false 29 | end 30 | end 31 | 32 | def self.is_ip_whitelist_retrieved 33 | !@@use_ip_whitelist || !@@ip_whitelist_rules.nil? 34 | end 35 | 36 | def self.is_ip_valid(ip) 37 | if @@use_ip_whitelist 38 | return ForestLiana::IpWhitelistChecker.is_ip_matches_any_rule(ip, @@ip_whitelist_rules) 39 | end 40 | 41 | true 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/services/forest_liana/ability.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Ability 3 | include ForestLiana::Ability::Permission 4 | 5 | ALLOWED_PERMISSION_LEVELS = %w[admin editor developer].freeze 6 | 7 | def forest_authorize!(action, user, collection, args = {}) 8 | case action 9 | when 'browse', 'read', 'edit', 'add', 'delete', 'export' 10 | raise ForestLiana::Ability::Exceptions::AccessDenied.new unless is_crud_authorized?(action, user, collection) 11 | when 'chart' 12 | if ALLOWED_PERMISSION_LEVELS.exclude?(user['permission_level']) 13 | raise ForestLiana::Errors::HTTP422Error.new('The argument parameters is missing') if args[:parameters].nil? 14 | raise ForestLiana::Ability::Exceptions::AccessDenied.new unless is_chart_authorized?(user, args[:parameters]) 15 | end 16 | when 'action' 17 | validate_collection collection 18 | raise ForestLiana::Errors::HTTP422Error.new('You must implement the arguments : parameters, endpoint & http_method') if args[:parameters].nil? || args[:endpoint].nil? || args[:http_method].nil? 19 | is_smart_action_authorized?(user, collection, args[:parameters], args[:endpoint], args[:http_method]) 20 | else 21 | raise ForestLiana::Ability::Exceptions::AccessDenied.new 22 | end 23 | end 24 | 25 | private 26 | 27 | def validate_collection(collection) 28 | if collection.nil? || !SchemaUtils.model_included?(collection) 29 | raise ForestLiana::Errors::HTTP422Error.new('The conditional smart actions are not supported with Smart Collection. Please contact an administrator.') 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/services/forest_liana/oidc_client_manager.rb: -------------------------------------------------------------------------------- 1 | require 'openid_connect' 2 | 3 | module ForestLiana 4 | class OidcClientManager 5 | def self.get_client 6 | begin 7 | configuration = ForestLiana::OidcConfigurationRetriever.retrieve() 8 | if ForestLiana.forest_client_id.nil? 9 | client_data = Rails.cache.read("#{ForestLiana.env_secret}-client-data") || nil 10 | if client_data.nil? 11 | client_credentials = ForestLiana::OidcDynamicClientRegistrator.register({ 12 | token_endpoint_auth_method: 'none', 13 | registration_endpoint: configuration['registration_endpoint'] 14 | }) 15 | client_data = { :client_id => client_credentials['client_id'], :issuer => configuration['issuer'], :redirect_uri => client_credentials['redirect_uris'][0] } 16 | Rails.cache.write("#{ForestLiana.env_secret}-client-data", client_data) 17 | end 18 | else 19 | client_data = { :client_id => ForestLiana.forest_client_id, :issuer => configuration['issuer'], :redirect_uri => File.join(ForestLiana.application_url, "/forest/authentication/callback").to_s } 20 | end 21 | 22 | OpenIDConnect::Client.new( 23 | identifier: client_data[:client_id], 24 | redirect_uri: client_data[:redirect_uri], 25 | host: "#{client_data[:issuer].sub(/^https?\:\/\/(www.)?/,'')}", 26 | authorization_endpoint: '/oidc/auth', 27 | token_endpoint: '/oidc/token', 28 | ) 29 | rescue => error 30 | Rails.cache.delete("#{ForestLiana.env_secret}-client-data") 31 | raise error 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/services/forest_liana/value_stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ValueStatGetter < StatGetter 3 | attr_accessor :record 4 | 5 | def perform 6 | return if @params[:aggregator].blank? 7 | resource = optimize_record_loading(@resource, get_resource) 8 | 9 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables']) 10 | 11 | unless filters.blank? 12 | filter_parser = FiltersParser.new(filters, resource, @params[:timezone], @params) 13 | resource = filter_parser.apply_filters 14 | raw_previous_interval = filter_parser.get_previous_interval_condition 15 | 16 | if raw_previous_interval 17 | previous_value = filter_parser.apply_filters_on_previous_interval(raw_previous_interval) 18 | end 19 | end 20 | 21 | @record = Model::Stat.new(value: { 22 | countCurrent: aggregate(resource), 23 | countPrevious: previous_value ? aggregate(previous_value) : nil 24 | }) 25 | end 26 | 27 | private 28 | 29 | def aggregate(value) 30 | aggregator = @params[:aggregator].downcase 31 | uniq = aggregator == 'count' 32 | 33 | if Rails::VERSION::MAJOR >= 4 34 | if uniq 35 | # NOTICE: uniq is deprecated since Rails 5.0 36 | value = Rails::VERSION::MAJOR >= 5 ? value.distinct : value.uniq 37 | end 38 | value.send(aggregator, aggregate_field) 39 | else 40 | value.send(aggregator, aggregate_field, distinct: uniq) 41 | end 42 | end 43 | 44 | def aggregate_field 45 | @params[:aggregateFieldName] || @resource.primary_key 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /config/initializers/error-messages.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | MESSAGES = { 3 | CONFIGURATION: { 4 | AUTH_SECRET_MISSING: "Your Forest authSecret seems to be missing. Can you check that you properly set a Forest authSecret in the Forest initializer?", 5 | }, 6 | SERVER_TRANSACTION: { 7 | SECRET_AND_RENDERINGID_INCONSISTENT: "Cannot retrieve the project you're trying to unlock. The envSecret and renderingId seems to be missing or inconsistent.", 8 | SERVER_DOWN: "Cannot retrieve the data from the Forest server. Forest API seems to be down right now.", 9 | SECRET_NOT_FOUND: "Cannot retrieve the data from the Forest server. Can you check that you properly copied the Forest envSecret in the Liana initializer?", 10 | UNEXPECTED: "Cannot retrieve the data from the Forest server. An error occured in Forest API.", 11 | INVALID_STATE_MISSING: "Invalid response from the authentication server: the state parameter is missing", 12 | INVALID_STATE_FORMAT: "Invalid response from the authentication server: the state parameter is not at the right format", 13 | INVALID_STATE_RENDERING_ID: "Invalid response from the authentication server: the state does not contain a renderingId", 14 | MISSING_RENDERING_ID: "Authentication request must contain a renderingId", 15 | INVALID_RENDERING_ID: "The parameter renderingId is not valid", 16 | REGISTRATION_FAILED: "The registration to the authentication API failed, response: ", 17 | OIDC_CONFIGURATION_RETRIEVAL_FAILED: "Failed to retrieve the provider's configuration.", 18 | names: { 19 | TWO_FACTOR_AUTHENTICATION_REQUIRED: 'TwoFactorAuthenticationRequiredForbiddenError', 20 | } 21 | } 22 | } 23 | end 24 | -------------------------------------------------------------------------------- /app/services/forest_liana/intercom_conversations_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomConversationsGetter < IntegrationBaseGetter 3 | def initialize(params) 4 | @params = params 5 | @access_token = ForestLiana.integrations[:intercom][:access_token] 6 | @intercom = ::Intercom::Client.new(token: @access_token) 7 | end 8 | 9 | def count 10 | @records.count 11 | end 12 | 13 | def records 14 | @records[pagination].map do |conversation| 15 | if conversation.assignee.is_a?(::Intercom::Admin) 16 | admins = @intercom.admins.all.detect(id: conversation.assignee.id) 17 | conversation.assignee = admins.first 18 | end 19 | conversation 20 | end 21 | end 22 | 23 | def perform 24 | begin 25 | resource = collection.find(@params[:id]) 26 | @records = @intercom.conversations.find_all( 27 | email: resource.email, 28 | type: 'user', 29 | display_as: 'plaintext', 30 | ).entries 31 | rescue Intercom::ResourceNotFound 32 | @records = [] 33 | rescue Intercom::UnexpectedError => exception 34 | FOREST_REPORTER.report exception 35 | FOREST_LOGGER.error "Cannot retrieve the Intercom conversations: #{exception.message}" 36 | @records = [] 37 | end 38 | end 39 | 40 | private 41 | 42 | def pagination 43 | offset..(offset + limit - 1) 44 | end 45 | 46 | def offset 47 | return 0 unless pagination? 48 | 49 | number = @params[:page][:number] 50 | if number && number.to_i > 0 51 | (number.to_i - 1) * limit 52 | else 53 | 0 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/services/forest_liana/line_stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class LineStatGetter < StatGetter 3 | attr_accessor :record 4 | 5 | def client_timezone 6 | # As stated here https://github.com/ankane/groupdate#for-sqlite 7 | # groupdate does not handle timezone for SQLite 8 | return false if 'SQLite' == ActiveRecord::Base.connection.adapter_name 9 | @params[:timezone] 10 | end 11 | 12 | def get_format 13 | case @params[:timeRange].try(:downcase) 14 | when 'day' 15 | '%d/%m/%Y' 16 | when 'week' 17 | 'W%V-%Y' 18 | when 'month' 19 | '%b %Y' 20 | when 'year' 21 | '%Y' 22 | end 23 | end 24 | 25 | def perform 26 | value = get_resource() 27 | 28 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables']) 29 | 30 | unless filters.blank? 31 | value = FiltersParser.new(filters, @resource, @params[:timezone], @params).apply_filters 32 | end 33 | 34 | Groupdate.week_start = :monday 35 | 36 | value = value.send(timeRange, group_by_date_field, time_zone: client_timezone) 37 | 38 | value = value.send(@params[:aggregator].downcase, @params[:aggregateFieldName]) 39 | .map do |k, v| 40 | { label: k.strftime(get_format), values: { value: v }} 41 | end 42 | 43 | @record = Model::Stat.new(value: value) 44 | end 45 | 46 | private 47 | 48 | def group_by_date_field 49 | "#{@resource.table_name}.#{@params[:groupByFieldName]}" 50 | end 51 | 52 | def timeRange 53 | "group_by_#{@params[:timeRange].try(:downcase) || 'month'}" 54 | end 55 | 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/widgets_helper.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module ForestLiana 4 | module WidgetsHelper 5 | 6 | @widget_edit_list = [ 7 | 'address editor', 8 | 'belongsto typeahead', 9 | 'belongsto dropdown', 10 | 'boolean editor', 11 | 'checkboxes', 12 | 'color editor', 13 | 'date editor', 14 | 'dropdown', 15 | 'embedded document editor', 16 | 'file picker', 17 | 'json code editor', 18 | 'input array', 19 | 'multiple select', 20 | 'number input', 21 | 'point editor', 22 | 'price editor', 23 | 'radio button', 24 | 'rich text', 25 | 'text area editor', 26 | 'text editor', 27 | 'time input', 28 | ] 29 | 30 | @v1_to_v2_edit_widgets_mapping = { 31 | address: 'address editor', 32 | 'belongsto select': 'belongsto dropdown', 33 | 'color picker': 'color editor', 34 | 'date picker': 'date editor', 35 | price: 'price editor', 36 | 'JSON editor': 'json code editor', 37 | 'rich text editor': 'rich text', 38 | 'text area': 'text area editor', 39 | 'text input': 'text editor', 40 | } 41 | 42 | def self.set_field_widget(field) 43 | 44 | if field[:widget] 45 | if @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym] 46 | field[:widgetEdit] = {name: @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym], parameters: {}} 47 | elsif @widget_edit_list.include?(field[:widget]) 48 | field[:widgetEdit] = {name: field[:widget], parameters: {}} 49 | end 50 | end 51 | 52 | if !field.key?(:widgetEdit) 53 | field[:widgetEdit] = nil 54 | end 55 | 56 | field.delete(:widget) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/forest_liana/json_printer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module JsonPrinter 3 | def pretty_print json, indentation = "" 4 | result = "" 5 | 6 | if json.kind_of? Array 7 | result << "[" 8 | is_small = json.length < 3 9 | is_primary_value = false 10 | 11 | json.each_index do |index| 12 | item = json[index] 13 | is_primary_value = !item.kind_of?(Hash) && !item.kind_of?(Array) 14 | 15 | if index == 0 && is_primary_value && !is_small 16 | result << "\n#{indentation} " 17 | elsif index > 0 && is_primary_value && !is_small 18 | result << ",\n#{indentation} " 19 | elsif index > 0 20 | result << ", " 21 | end 22 | 23 | result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation) 24 | end 25 | 26 | result << "\n#{indentation}" if is_primary_value && !is_small 27 | result << "]" 28 | elsif json.kind_of? Hash 29 | result << "{\n" 30 | 31 | is_first = true 32 | json = json.stringify_keys 33 | json.each do |key, value| 34 | result << ",\n" unless is_first 35 | is_first = false 36 | result << "#{indentation} \"#{key}\": " 37 | result << pretty_print(value, "#{indentation} ") 38 | end 39 | 40 | result << "\n#{indentation}}" 41 | elsif json.nil? 42 | result << "null" 43 | elsif !!json == json 44 | result << (json ? "true" : "false") 45 | elsif json.is_a?(String) || json.is_a?(Symbol) 46 | result << "\"#{json.gsub(/"/, '\"')}\"" 47 | else 48 | result << json.to_s 49 | end 50 | 51 | result 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/adapter_helper.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module AdapterHelper 3 | ADAPTER_MYSQL2 = 'Mysql2' 4 | 5 | def self.format_column_name(table_name, column_name) 6 | quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table_name) 7 | quoted_column_name = ActiveRecord::Base.connection.quote_column_name(column_name) 8 | "#{quoted_table_name}.#{quoted_column_name}" 9 | end 10 | 11 | def self.cast_boolean(value) 12 | if ['MySQL', 'SQLite'].include?(ActiveRecord::Base.connection.adapter_name) 13 | value === 'true' ? 1 : 0 14 | else 15 | value 16 | end 17 | end 18 | 19 | def self.format_live_query_value_result(result) 20 | # NOTICE: The adapters have their own specific format for the live query value chart results. 21 | case ActiveRecord::Base.connection.adapter_name 22 | when ADAPTER_MYSQL2 23 | { 'value' => result.first.first } 24 | else 25 | result.first 26 | end 27 | end 28 | 29 | def self.format_live_query_pie_result(result) 30 | # NOTICE: The adapters have their own specific format for the live query pie chart results. 31 | case ActiveRecord::Base.connection.adapter_name 32 | when ADAPTER_MYSQL2 33 | result.map { |value| { 'key' => value[0], 'value' => value[1] } } 34 | else 35 | result.to_a 36 | end 37 | end 38 | 39 | def self.format_live_query_line_result(result) 40 | # NOTICE: The adapters have their own specific format for the live query line chart results. 41 | case ActiveRecord::Base.connection.adapter_name 42 | when ADAPTER_MYSQL2 43 | result.map { |value| { 'key' => value[0], 'value' => value[1] } } 44 | else 45 | result 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/services/forest_liana/utils/context_variables_injector.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module Utils 3 | class ContextVariablesInjector 4 | 5 | def self.inject_context_in_value(value, context_variables) 6 | inject_context_in_value_custom(value) do |context_variable_key| 7 | context_variables.get_value(context_variable_key).to_s 8 | end 9 | end 10 | 11 | def self.inject_context_in_value_custom(value) 12 | return value unless value.is_a?(String) 13 | 14 | value_with_context_variables_injected = value 15 | regex = /{{([^}]+)}}/ 16 | encountered_variables = [] 17 | 18 | while (match = regex.match(value_with_context_variables_injected)) 19 | context_variable_key = match[1] 20 | 21 | unless encountered_variables.include?(context_variable_key) 22 | value_with_context_variables_injected.gsub!( 23 | /{{#{context_variable_key}}}/, 24 | yield(context_variable_key) 25 | ) 26 | end 27 | 28 | encountered_variables.push(context_variable_key) 29 | end 30 | 31 | value_with_context_variables_injected 32 | end 33 | 34 | def self.inject_context_in_filter(filter, context_variables) 35 | return nil unless filter 36 | 37 | if filter.key? 'aggregator' 38 | return { 39 | 'aggregator' => filter['aggregator'], 40 | 'conditions' => filter['conditions'].map { |condition| inject_context_in_filter(condition, context_variables) } 41 | } 42 | end 43 | 44 | { 45 | 'field' => filter['field'], 46 | 'operator' => filter['operator'], 47 | 'value' => inject_context_in_value(filter['value'], context_variables) 48 | } 49 | 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/services/forest_liana/authentication.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class Authentication 3 | def start_authentication(state) 4 | client = ForestLiana::OidcClientManager.get_client() 5 | 6 | authorization_url = client.authorization_uri({ 7 | scope: 'openid email profile', 8 | state: state.to_s, 9 | }) 10 | 11 | { 'authorization_url' => authorization_url } 12 | end 13 | 14 | def verify_code_and_generate_token(params) 15 | client = ForestLiana::OidcClientManager.get_client() 16 | 17 | rendering_id = parse_state(params['state']) 18 | client.authorization_code = params['code'] 19 | 20 | if Rails.env.development? || Rails.env.test? 21 | OpenIDConnect.http_config do |config| 22 | config.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE 23 | end 24 | end 25 | access_token_instance = client.access_token! 'none' 26 | 27 | user = ForestLiana::AuthorizationGetter.authenticate( 28 | rendering_id, 29 | { :forest_token => access_token_instance.instance_variable_get(:@access_token) }, 30 | ) 31 | 32 | return ForestLiana::Token.create_token(user, rendering_id) 33 | end 34 | 35 | private 36 | def parse_state(state) 37 | unless state 38 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_MISSING] 39 | end 40 | 41 | begin 42 | parsed_state = JSON.parse(state.gsub("'",'"').gsub('=>',':')) 43 | rendering_id = parsed_state["renderingId"].to_s 44 | rescue 45 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_FORMAT] 46 | end 47 | 48 | if rendering_id.nil? 49 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_RENDERING_ID] 50 | end 51 | 52 | return rendering_id 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/controllers/forest_liana/router.rb: -------------------------------------------------------------------------------- 1 | class ForestLiana::Router 2 | def call(env) 3 | params = env['action_dispatch.request.path_parameters'] 4 | collection_name = params[:collection] 5 | resource = ForestLiana::SchemaUtils.find_model_from_collection_name(collection_name, true) 6 | 7 | if resource.nil? 8 | FOREST_LOGGER.error "Routing error: Resource not found for collection #{collection_name}." 9 | FOREST_LOGGER.error "If this is a Smart Collection, please ensure your Smart Collection routes are defined before the mounted ForestLiana::Engine?" 10 | ForestLiana::BaseController.action(:route_not_found).call(env) 11 | else 12 | begin 13 | component_prefix = ForestLiana.component_prefix(resource) 14 | controller_name = "#{component_prefix}Controller" 15 | 16 | controller = "ForestLiana::UserSpace::#{controller_name}".constantize 17 | action = nil 18 | 19 | case env['REQUEST_METHOD'] 20 | when 'GET' 21 | if params[:id] 22 | action = 'show' 23 | elsif env['PATH_INFO'] == "/#{collection_name}/count" 24 | action = 'count' 25 | else 26 | action = 'index' 27 | end 28 | when 'PUT' 29 | action = 'update' 30 | when 'POST' 31 | action = 'create' 32 | when 'DELETE' 33 | if params[:id] 34 | action = 'destroy' 35 | else 36 | action = 'destroy_bulk' 37 | end 38 | end 39 | 40 | controller.action(action.to_sym).call(env) 41 | rescue NoMethodError => exception 42 | FOREST_REPORTER.report exception 43 | FOREST_LOGGER.error "Routing error: #{exception}\n#{exception.backtrace.join("\n\t")}" 44 | ForestLiana::BaseController.action(:route_not_found).call(env) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/requests/cors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'rack/test' 3 | require 'rack/cors' 4 | 5 | class CaptureResult 6 | def initialize(app, options = {}) 7 | @app = app 8 | @result_holder = options[:holder] 9 | end 10 | 11 | def call(env) 12 | env['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'] = 'true' 13 | response = @app.call(env) 14 | @result_holder.cors_result = env[Rack::Cors::RACK_CORS] 15 | response 16 | end 17 | end 18 | 19 | describe Rack::Cors do 20 | 21 | include Rack::Test::Methods 22 | 23 | attr_accessor :cors_result 24 | 25 | def load_app(name, options = {}) 26 | test = self 27 | Rack::Builder.new do 28 | use CaptureResult, holder: test 29 | eval File.read(File.dirname(__FILE__) + "/#{name}.ru") 30 | use FakeProxy if options[:proxy] 31 | map('/') do 32 | run(lambda do |_env| 33 | [ 34 | 200, 35 | { 36 | 'Content-Type' => 'text/html', 37 | }, 38 | ['success'] 39 | ] 40 | end) 41 | end 42 | end 43 | end 44 | 45 | let(:app) { load_app('test') } 46 | 47 | describe 'preflight requests' do 48 | it 'should allow private network' do 49 | preflight_request('http://localhost:3000', '/') 50 | assert !last_response.headers['Access-Control-Allow-Private-Network'].nil? 51 | assert last_response.headers['Access-Control-Allow-Private-Network'] == 'true' 52 | end 53 | end 54 | 55 | protected 56 | 57 | def preflight_request(origin, path, opts = {}) 58 | header 'Origin', origin 59 | unless opts.key?(:method) && opts[:method].nil? 60 | header 'Access-Control-Request-Method', opts[:method] ? opts[:method].to_s.upcase : 'GET' 61 | end 62 | header 'Access-Control-Request-Headers', opts[:headers] if opts[:headers] 63 | options path 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_sources_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeSourcesGetter < StripeBaseGetter 3 | attr_accessor :records 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def count 11 | @cards.try(:total_count) || 0 12 | end 13 | 14 | def perform 15 | params = { 16 | limit: limit, 17 | starting_after: starting_after, 18 | ending_before: ending_before, 19 | object: @params[:object] 20 | } 21 | params['include[]'] = 'total_count' 22 | 23 | resource = collection.find(@params[:id]) 24 | customer = resource[field] 25 | 26 | if customer.blank? 27 | @records = [] 28 | else 29 | fetch_bank_accounts(customer, params) 30 | end 31 | end 32 | 33 | def fetch_bank_accounts(customer, params) 34 | begin 35 | @cards = ::Stripe::Customer.retrieve({ id: customer, expand: ['sources'] }).sources.list(params) 36 | if @cards.blank? 37 | @records = [] 38 | return 39 | end 40 | 41 | @records = @cards.data.map do |d| 42 | query = {} 43 | query[field] = d.customer 44 | d.customer = collection.find_by(query) 45 | 46 | d 47 | end 48 | rescue ::Stripe::InvalidRequestError => error 49 | FOREST_REPORTER.report error 50 | FOREST_LOGGER.error "Stripe error: #{error.message}" 51 | @records = [] 52 | end 53 | end 54 | 55 | def starting_after 56 | if pagination? && @params[:starting_after] 57 | @params[:starting_after] 58 | end 59 | end 60 | 61 | def ending_before 62 | if pagination? && @params[:ending_before] 63 | @params[:ending_before] 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/helpers/forest_liana/query_helper.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | module QueryHelper 3 | def self.get_one_associations(resource) 4 | associations = SchemaUtils.one_associations(resource) 5 | .select do |association| 6 | if SchemaUtils.polymorphic?(association) 7 | SchemaUtils.polymorphic_models(association).all? { |model| SchemaUtils.model_included?(model) } 8 | else 9 | SchemaUtils.model_included?(association.klass) 10 | end 11 | end 12 | 13 | associations 14 | end 15 | 16 | def self.get_one_association_names_symbol(resource) 17 | self.get_one_associations(resource).map(&:name) 18 | end 19 | 20 | def self.get_one_association_names_string(resource) 21 | self.get_one_associations(resource).map { |association| association.name.to_s } 22 | end 23 | 24 | def self.get_tables_associated_to_relations_name(resource) 25 | tables_associated_to_relations_name = {} 26 | associations_has_one = self.get_one_associations(resource) 27 | 28 | associations_has_one.each do |association| 29 | if SchemaUtils.polymorphic?(association) 30 | SchemaUtils.polymorphic_models(association).each do |model| 31 | if tables_associated_to_relations_name[model.table_name].nil? 32 | tables_associated_to_relations_name[model.table_name] = [] 33 | end 34 | tables_associated_to_relations_name[model.table_name] << association.name 35 | end 36 | else 37 | if tables_associated_to_relations_name[association.try(:table_name)].nil? 38 | tables_associated_to_relations_name[association.table_name] = [] 39 | end 40 | tables_associated_to_relations_name[association.table_name] << association.name 41 | end 42 | end 43 | 44 | tables_associated_to_relations_name 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /test/fixtures/string_field.yml: -------------------------------------------------------------------------------- 1 | string_field_1: 2 | id: 1 3 | field: Test 1 4 | 5 | string_field_2: 6 | id: 2 7 | field: Test 2 8 | 9 | string_field_3: 10 | id: 3 11 | field: Test 3 12 | 13 | string_field_4: 14 | id: 4 15 | field: Test 4 16 | 17 | string_field_5: 18 | id: 5 19 | field: Test 5 20 | 21 | string_field_6: 22 | id: 6 23 | field: Test 6 24 | 25 | string_field_7: 26 | id: 7 27 | field: Test 7 28 | 29 | string_field_8: 30 | id: 8 31 | field: Test 8 32 | 33 | string_field_9: 34 | id: 9 35 | field: Test 9 36 | 37 | string_field_10: 38 | id: 10 39 | field: Test 10 40 | 41 | string_field_11: 42 | id: 11 43 | field: Test 11 44 | 45 | string_field_12: 46 | id: 12 47 | field: Test 12 48 | 49 | string_field_13: 50 | id: 13 51 | field: Test 13 52 | 53 | string_field_14: 54 | id: 14 55 | field: Test 14 56 | 57 | string_field_15: 58 | id: 15 59 | field: Test 15 60 | 61 | string_field_16: 62 | id: 16 63 | field: Test 16 64 | 65 | string_field_17: 66 | id: 17 67 | field: Test 17 68 | 69 | string_field_18: 70 | id: 18 71 | field: Test 18 72 | 73 | string_field_19: 74 | id: 19 75 | field: Test 19 76 | 77 | string_field_20: 78 | id: 20 79 | field: Test 20 80 | 81 | string_field_21: 82 | id: 21 83 | field: Test 21 84 | 85 | string_field_22: 86 | id: 22 87 | field: Test 22 88 | 89 | string_field_23: 90 | id: 23 91 | field: Test 23 92 | 93 | string_field_24: 94 | id: 24 95 | field: Test 24 96 | 97 | string_field_25: 98 | id: 25 99 | field: Test 25 100 | 101 | string_field_26: 102 | id: 26 103 | field: Test 26 104 | 105 | string_field_27: 106 | id: 27 107 | field: Test 27 108 | 109 | string_field_28: 110 | id: 28 111 | field: Test 28 112 | 113 | string_field_29: 114 | id: 29 115 | field: Test 29 116 | 117 | string_field_30: 118 | id: 30 119 | field: Test 30 120 | -------------------------------------------------------------------------------- /app/services/forest_liana/leaderboard_stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class LeaderboardStatGetter < StatGetter 3 | def initialize(parent_model, params, forest_user) 4 | @scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone]) 5 | child_model = @scoped_parent_model.reflect_on_association(params[:relationshipFieldName]).klass 6 | @scoped_child_model = get_scoped_model(child_model, forest_user, params[:timezone]) 7 | @label_field = params[:labelFieldName] 8 | @aggregate = params[:aggregator].downcase 9 | @aggregate_field = params[:aggregateFieldName] 10 | @limit = params[:limit] 11 | @group_by = "#{@scoped_parent_model.table_name}.#{@label_field}" 12 | end 13 | 14 | def perform 15 | includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@scoped_child_model) 16 | 17 | result = @scoped_child_model 18 | .joins(includes) 19 | .where({ @scoped_parent_model.name.downcase.to_sym => @scoped_parent_model }) 20 | .group(@group_by) 21 | .order(order) 22 | .limit(@limit) 23 | .send(@aggregate, @aggregate_field) 24 | .map { |key, value| { key: key, value: value } } 25 | 26 | @record = Model::Stat.new(value: result) 27 | end 28 | 29 | def get_scoped_model(model, forest_user, timezone) 30 | scope_filters = ForestLiana::ScopeManager.get_scope(model.name, forest_user) 31 | 32 | return model.unscoped if scope_filters.blank? 33 | 34 | FiltersParser.new(scope_filters, model, timezone, @params).apply_filters 35 | end 36 | 37 | def order 38 | order = 'DESC' 39 | 40 | # NOTICE: The generated alias for a count is "count_all", for a sum the 41 | # alias looks like "sum_#{aggregate_field}" 42 | if @aggregate == 'sum' 43 | field = @aggregate_field.downcase 44 | else 45 | field = 'all' 46 | end 47 | "#{@aggregate}_#{field} #{order}" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_subscriptions_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeSubscriptionsGetter < StripeBaseGetter 3 | attr_accessor :records 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def count 11 | @subscriptions.try(:total_count) || 0 12 | end 13 | 14 | def perform 15 | begin 16 | query = { 17 | limit: limit, 18 | starting_after: starting_after, 19 | ending_before: ending_before 20 | } 21 | 22 | if @params[:id] && collection && field 23 | resource = collection.find(@params[:id]) 24 | query[:customer] = resource[field] 25 | end 26 | 27 | query['include[]'] = 'total_count' 28 | @subscriptions = fetch_subscriptions(query) 29 | if @subscriptions.blank? 30 | @records = [] 31 | return 32 | end 33 | 34 | @records = @subscriptions.data.map do |d| 35 | query = {} 36 | query[field] = d.customer 37 | if collection 38 | d.customer = collection.find_by(query) 39 | else 40 | d.customer = nil 41 | end 42 | 43 | d 44 | end 45 | rescue ::Stripe::InvalidRequestError => error 46 | FOREST_REPORTER.report error 47 | FOREST_LOGGER.error "Stripe error: #{error.message}" 48 | @records = [] 49 | end 50 | end 51 | 52 | def fetch_subscriptions(params) 53 | return if @params[:id] && params[:customer].blank? 54 | ::Stripe::Subscription.list(params) 55 | end 56 | 57 | def starting_after 58 | if pagination? && @params[:starting_after] 59 | @params[:starting_after] 60 | end 61 | end 62 | 63 | def ending_before 64 | if pagination? && @params[:ending_before] 65 | @params[:ending_before] 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/services/forest_liana/resource_creator.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class ResourceCreator 3 | attr_accessor :record 4 | attr_accessor :errors 5 | 6 | def initialize(resource, params) 7 | @resource = resource 8 | @params = params 9 | @errors = nil 10 | end 11 | 12 | def perform 13 | begin 14 | if has_strong_parameter 15 | @record = @resource.create(resource_params) 16 | else 17 | @record = @resource.create(resource_params, without_protection: true) 18 | end 19 | set_has_many_relationships 20 | rescue ActiveRecord::StatementInvalid => exception 21 | # NOTICE: SQL request cannot be executed properly 22 | @errors = [{ detail: exception.cause.error }] 23 | rescue ForestLiana::Errors::SerializeAttributeBadFormat => exception 24 | @errors = [{ detail: exception.message }] 25 | rescue => exception 26 | @errors = [{ detail: exception.message }] 27 | end 28 | end 29 | 30 | def resource_params 31 | ResourceDeserializer.new(@resource, @params, true).perform 32 | end 33 | 34 | def set_has_many_relationships 35 | if @params['data']['relationships'] 36 | @params['data']['relationships'].each do |name, relationship| 37 | data = relationship['data'] 38 | association = @resource.reflect_on_association(name.to_sym) 39 | if [:has_many, :has_and_belongs_to_many].include?( 40 | association.try(:macro)) 41 | if data.is_a?(Array) 42 | data.each do |x| 43 | existing_records = @record.send(name) 44 | new_record = association.klass.find(x[:id]) 45 | if !existing_records.include?(new_record) 46 | existing_records << new_record 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | 55 | def has_strong_parameter 56 | Rails::VERSION::MAJOR > 5 || @resource.instance_method(:update!).arity == 1 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/services/forest_liana/oidc_dynamic_client_registrator.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'json/jwt' 3 | 4 | module ForestLiana 5 | class OidcDynamicClientRegistrator 6 | def self.is_standard_body_error(response) 7 | result = false 8 | begin 9 | jsonbody 10 | 11 | if (!response['body'].is_a?(Object) || response['body'].is_a?(StringIO)) 12 | jsonbody = JSON.parse(response['body']) 13 | else 14 | jsonbody = response['body'] 15 | end 16 | 17 | result = jsonbody['error'].is_a?(String) && jsonbody['error'].length > 0 18 | 19 | if (result) 20 | response['body'] = jsonbody 21 | end 22 | rescue 23 | {} 24 | end 25 | 26 | return result 27 | end 28 | 29 | def self.process_response(response, expected = {}) 30 | statusCode = expected[:statusCode] || 200 31 | body = expected[:body] || true 32 | 33 | if (response.code.to_i != statusCode.to_i) 34 | if (is_standard_body_error(response)) 35 | raise response['body'] 36 | end 37 | 38 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body 39 | end 40 | 41 | if (body && !response.body) 42 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body 43 | end 44 | 45 | return response.body 46 | end 47 | 48 | def self.authorization_header_value(token, tokenType = 'Bearer') 49 | return "#{tokenType} #{token}" 50 | end 51 | 52 | def self.register(metadata) 53 | initial_access_token = ForestLiana.env_secret 54 | 55 | response = ForestLiana::ForestApiRequester.post( 56 | metadata[:registration_endpoint], 57 | body: metadata, 58 | headers: initial_access_token ? { 59 | Authorization: authorization_header_value(initial_access_token), 60 | } : {}, 61 | ) 62 | 63 | responseBody = process_response(response, { :statusCode => 201, :bearer => true }) 64 | return JSON.parse(responseBody) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_payments_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripePaymentsGetter < StripeBaseGetter 3 | attr_accessor :records 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def count 11 | @charges.try(:total_count) || 0 12 | end 13 | 14 | def perform 15 | begin 16 | query = { 17 | limit: limit, 18 | starting_after: starting_after, 19 | ending_before: ending_before 20 | } 21 | 22 | if @params[:id] && collection && field 23 | resource = collection.find(@params[:id]) 24 | query[:customer] = resource[field] 25 | end 26 | 27 | query['source'] = { object: :card } 28 | query['include[]'] = 'total_count' 29 | 30 | @charges = fetch_charges(query) 31 | if @charges.blank? 32 | @records = [] 33 | return 34 | end 35 | 36 | @records = @charges.data.map do |d| 37 | d.created = Time.at(d.created).to_datetime 38 | d.amount /= 100.00 39 | 40 | query = {} 41 | query[field] = d.customer 42 | if collection 43 | d.customer = collection.find_by(query) 44 | else 45 | d.customer = nil 46 | end 47 | 48 | d 49 | end 50 | rescue ::Stripe::InvalidRequestError => error 51 | FOREST_REPORTER.report error 52 | FOREST_LOGGER.error "Stripe error: #{error.message}" 53 | @records = [] 54 | end 55 | end 56 | 57 | def fetch_charges(params) 58 | return if @params[:id] && params[:customer].blank? 59 | ::Stripe::Charge.list(params) 60 | end 61 | 62 | def starting_after 63 | if pagination? && @params[:starting_after] 64 | @params[:starting_after] 65 | end 66 | end 67 | 68 | def ending_before 69 | if pagination? && @params[:ending_before] 70 | @params[:ending_before] 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/intercom_attribute_serializer.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class IntercomAttributeSerializer 3 | include ForestAdmin::JSONAPI::Serializer 4 | 5 | attribute :session_count 6 | attribute :last_seen_ip 7 | 8 | attribute :created_at do 9 | object.created_at.try(:utc).try(:iso8601) 10 | end 11 | 12 | attribute :updated_at do 13 | object.updated_at.try(:utc).try(:iso8601) 14 | end 15 | 16 | attribute :signed_up_at do 17 | object.signed_up_at.try(:utc).try(:iso8601) 18 | end 19 | 20 | attribute :last_request_at do 21 | object.last_request_at.try(:utc).try(:iso8601) 22 | end 23 | 24 | attribute :country do 25 | object.location_data.try(:country_name) 26 | end 27 | 28 | attribute :city do 29 | object.location_data.try(:city_name) 30 | end 31 | 32 | attribute :user_agent do 33 | object.user_agent_data 34 | end 35 | 36 | attribute :companies do 37 | object.companies.map(&:name) 38 | end 39 | 40 | attribute :segments do 41 | object.segments.map(&:name) 42 | end 43 | 44 | attribute :tags do 45 | object.tags.map(&:name) 46 | end 47 | 48 | attribute :browser do 49 | useragent = UserAgent.parse(object.user_agent_data) 50 | "#{useragent.try(:browser)} #{useragent.try(:version)}" 51 | end 52 | 53 | attribute :platform do 54 | UserAgent.parse(object.user_agent_data).try(:platform) 55 | end 56 | 57 | attribute :geoloc do 58 | [object.location_data.try(:latitude), object.location_data.try(:longitude)] 59 | end 60 | 61 | def self_link 62 | "/forest#{super}" 63 | end 64 | 65 | def type 66 | @options[:context][:type] || 'intercom-attributes' 67 | end 68 | 69 | def format_name(attribute_name) 70 | attribute_name.to_s 71 | end 72 | 73 | def unformat_name(attribute_name) 74 | attribute_name.to_s.underscore 75 | end 76 | 77 | def relationship_self_link(attribute_name) 78 | nil 79 | end 80 | 81 | def relationship_related_link(attribute_name) 82 | nil 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/forest_liana.rb: -------------------------------------------------------------------------------- 1 | require 'forest_liana/engine' 2 | 3 | module Forest 4 | end 5 | 6 | module ForestLiana 7 | 8 | autoload :MixpanelEvent, 'forest_liana/mixpanel_event' 9 | 10 | module UserSpace 11 | end 12 | 13 | # NOTICE: Deprecated secret value names 14 | mattr_accessor :secret_key 15 | mattr_accessor :auth_key 16 | 17 | mattr_accessor :env_secret 18 | mattr_accessor :auth_secret 19 | mattr_accessor :forest_client_id 20 | mattr_accessor :application_url 21 | mattr_accessor :integrations 22 | mattr_accessor :apimap 23 | mattr_accessor :allowed_users 24 | mattr_accessor :models 25 | mattr_accessor :excluded_models 26 | mattr_accessor :included_models 27 | mattr_accessor :user_class_name 28 | mattr_accessor :names_overriden 29 | mattr_accessor :meta 30 | mattr_accessor :logger 31 | mattr_accessor :reporter 32 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore. 33 | mattr_accessor :names_old_overriden 34 | 35 | self.apimap = [] 36 | self.allowed_users = [] 37 | self.models = [] 38 | self.excluded_models = [] 39 | self.included_models = [] 40 | self.user_class_name = nil 41 | self.names_overriden = {} 42 | self.meta = {} 43 | self.logger = nil 44 | self.reporter = nil 45 | 46 | @config_dir = 'lib/forest_liana/**/*.rb' 47 | 48 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore. 49 | self.names_old_overriden = {} 50 | 51 | def self.config_dir=(config_dir) 52 | @config_dir = config_dir 53 | end 54 | 55 | def self.config_dir 56 | Rails.root.join(@config_dir) 57 | end 58 | 59 | def self.schema_for_resource resource 60 | self.apimap.find do |collection| 61 | SchemaUtils.find_model_from_collection_name(collection.name) 62 | .try(:name) == resource.name 63 | end 64 | end 65 | 66 | def self.name_for(model) 67 | self.names_overriden[model] || model.try(:name).gsub('::', '__') 68 | end 69 | 70 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore. 71 | def self.name_old_for(model) 72 | self.names_old_overriden[model] || model.try(:table_name) 73 | end 74 | 75 | def self.component_prefix(model) 76 | self.name_for(model).classify 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /app/services/forest_liana/stripe_invoices_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class StripeInvoicesGetter < StripeBaseGetter 3 | attr_accessor :records 4 | 5 | def initialize(params, secret_key, reference) 6 | @params = params 7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key] 8 | end 9 | 10 | def count 11 | @invoices.try(:total_count) || 0 12 | end 13 | 14 | def perform 15 | begin 16 | query = { 17 | limit: limit, 18 | starting_after: starting_after, 19 | ending_before: ending_before 20 | } 21 | 22 | if @params[:id] && collection && field 23 | resource = collection.find(@params[:id]) 24 | query[:customer] = resource[field] 25 | end 26 | 27 | query['include[]'] = 'total_count' 28 | @invoices = fetch_invoices(query) 29 | if @invoices.blank? 30 | @records = [] 31 | return 32 | end 33 | 34 | @records = @invoices.data.map do |d| 35 | d.date = Time.at(d.created).to_datetime 36 | d.period_start = Time.at(d.period_start).to_datetime 37 | d.period_end = Time.at(d.period_end).to_datetime 38 | d.subtotal /= 100.00 39 | d.total /= 100.00 40 | d.amount_due /= 100.00 41 | 42 | query = {} 43 | query[field] = d.customer 44 | if collection 45 | d.customer = collection.find_by(query) 46 | else 47 | d.customer = nil 48 | end 49 | 50 | d 51 | end 52 | rescue ::Stripe::InvalidRequestError => error 53 | FOREST_REPORTER.report error 54 | FOREST_LOGGER.error "Stripe error: #{error.message}" 55 | @records = [] 56 | end 57 | end 58 | 59 | def fetch_invoices(params) 60 | return if @params[:id] && params[:customer].blank? 61 | ::Stripe::Invoice.list(params) 62 | end 63 | 64 | def starting_after 65 | if pagination? && @params[:starting_after] 66 | @params[:starting_after] 67 | end 68 | end 69 | 70 | def ending_before 71 | if pagination? && @params[:ending_before] 72 | @params[:ending_before] 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/services/forest_liana/ip_whitelist_checker.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddress' 2 | 3 | module ForestLiana 4 | class IpWhitelistChecker 5 | module RuleType 6 | IP = 0 7 | RANGE = 1 8 | SUBNET = 2 9 | end 10 | 11 | def self.is_ip_matches_any_rule(ip, rules) 12 | rules.any? { |rule| IpWhitelistChecker.is_ip_matches_rule(ip, rule) } 13 | end 14 | 15 | def self.is_ip_matches_rule(ip, rule) 16 | if rule['type'] == RuleType::IP 17 | return IpWhitelistChecker.is_ip_match_ip(ip, rule['ip']) 18 | elsif rule['type'] == RuleType::RANGE 19 | return IpWhitelistChecker.is_ip_match_range(ip, rule) 20 | elsif rule['type'] == RuleType::SUBNET 21 | return IpWhitelistChecker.is_ip_match_subnet(ip, rule['range']) 22 | end 23 | 24 | raise 'Invalid rule type' 25 | end 26 | 27 | def self.ip_version(ip) 28 | (IPAddress ip).is_a?(IPAddress::IPv4) ? :ip_v4 : :ip_v6 29 | end 30 | 31 | def self.is_same_ip_version(ip1, ip2) 32 | ip1_version = IpWhitelistChecker.ip_version(ip1) 33 | ip2_version = IpWhitelistChecker.ip_version(ip2) 34 | 35 | ip1_version == ip2_version 36 | end 37 | 38 | def self.is_both_loopback(ip1, ip2) 39 | IPAddress(ip1).loopback? && IPAddress(ip2).loopback? 40 | end 41 | 42 | def self.is_ip_match_ip(ip1, ip2) 43 | if !IpWhitelistChecker.is_same_ip_version(ip1, ip2) 44 | return IpWhitelistChecker.is_both_loopback(ip1, ip2) 45 | end 46 | 47 | if IPAddress(ip1) == IPAddress(ip2) 48 | true 49 | else 50 | IpWhitelistChecker.is_both_loopback(ip1, ip2) 51 | end 52 | end 53 | 54 | def self.is_ip_match_range(ip, rule) 55 | return false if !IpWhitelistChecker.is_same_ip_version(ip, rule['ip_minimum']) 56 | 57 | ip_range_minimum = (IPAddress rule['ip_minimum']).to_i 58 | ip_range_maximum = (IPAddress rule['ip_maximum']).to_i 59 | ip_value = (IPAddress ip).to_i 60 | 61 | return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum 62 | end 63 | 64 | def self.is_ip_match_subnet(ip, subnet) 65 | return false if !IpWhitelistChecker.is_same_ip_version(ip, subnet) 66 | 67 | IPAddress(subnet).include?(IPAddress(ip)) 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main', '+([0-9])?(.{+([0-9]),x}).x', {name: 'beta', prerelease: true}], 3 | plugins: [ 4 | [ 5 | '@semantic-release/commit-analyzer', { 6 | preset: 'angular', 7 | releaseRules: [ 8 | // Example: `type(scope): subject [force release]` 9 | { subject: '*\\[force release\\]*', release: 'patch' }, 10 | ], 11 | }, 12 | ], 13 | '@semantic-release/release-notes-generator', 14 | '@semantic-release/changelog', 15 | [ 16 | '@semantic-release/exec', 17 | { 18 | prepareCmd: 'sed -i \'s/forest_liana (.*)/forest_liana (${nextRelease.version})/g\' Gemfile.lock; sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' lib/forest_liana/version.rb; sed -i \'s/"version": ".*"/"version": "${nextRelease.version}"/g\' package.json;', 19 | successCmd: 'touch .trigger-rubygem-release', 20 | }, 21 | ], 22 | [ 23 | '@semantic-release/git', 24 | { 25 | assets: ['CHANGELOG.md', 'Gemfile.lock', 'lib/forest_liana/version.rb', 'package.json'], 26 | }, 27 | ], 28 | '@semantic-release/github', 29 | 'semantic-release-rubygem', 30 | [ 31 | 'semantic-release-slack-bot', 32 | { 33 | markdownReleaseNotes: true, 34 | notifyOnSuccess: true, 35 | notifyOnFail: false, 36 | onSuccessTemplate: { 37 | text: "📦 $package_name@$npm_package_version has been released!", 38 | blocks: [{ 39 | type: 'section', 40 | text: { 41 | type: 'mrkdwn', 42 | text: '*New `$package_name` package released!*' 43 | } 44 | }, { 45 | type: 'context', 46 | elements: [{ 47 | type: 'mrkdwn', 48 | text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>" 49 | }] 50 | }, { 51 | type: 'divider', 52 | }], 53 | attachments: [{ 54 | blocks: [{ 55 | type: 'section', 56 | text: { 57 | type: 'mrkdwn', 58 | text: '*Changes* of version $release_notes', 59 | }, 60 | }], 61 | }], 62 | }, 63 | packageName: 'forest_liana', 64 | } 65 | ], 66 | ], 67 | } 68 | -------------------------------------------------------------------------------- /app/serializers/forest_liana/schema_serializer.rb: -------------------------------------------------------------------------------- 1 | class ForestLiana::SchemaSerializer 2 | def initialize collections, meta 3 | @collections = collections 4 | @meta = meta 5 | @data = [] 6 | @included = [] 7 | end 8 | 9 | def serialize 10 | populate_data_and_included 11 | { 12 | data: @data, 13 | included: @included, 14 | meta: @meta 15 | } 16 | end 17 | 18 | private 19 | 20 | def populate_data_and_included 21 | @collections.each do |collection| 22 | serialize_collection(collection) 23 | end 24 | end 25 | 26 | def serialize_collection collection 27 | collection_serialized = { 28 | id: collection['name'], 29 | type: 'collections', 30 | attributes: {}, 31 | relationships: { 32 | actions: { 33 | data: [] 34 | }, 35 | segments: { 36 | data: [] 37 | } 38 | } 39 | } 40 | 41 | collection.each do |attribute, value| 42 | if attribute == 'actions' 43 | value.each do |action| 44 | action_id = define_child_id(collection_serialized[:id], action['name']) 45 | collection_serialized[:relationships][:actions][:data] << format_child_pointer('actions', action_id) 46 | @included << format_child_content('actions', action_id, action) 47 | end 48 | elsif attribute == 'segments' 49 | value.each do |segment| 50 | segment_id = define_child_id(collection_serialized[:id], segment['name']) 51 | collection_serialized[:relationships][:segments][:data] << format_child_pointer('segments', segment_id) 52 | @included << format_child_content('segments', segment_id, segment) 53 | end 54 | else 55 | collection_serialized[:attributes][attribute.to_sym] = value 56 | end 57 | end 58 | 59 | @data << collection_serialized 60 | end 61 | 62 | def define_child_id collection_id, object_id 63 | "#{collection_id}.#{object_id}" 64 | end 65 | 66 | def format_child_pointer type, id 67 | { id: id, type: type } 68 | end 69 | 70 | def format_child_content type, id, object 71 | child_serialized = { 72 | id: id, 73 | type: type, 74 | attributes: {} 75 | } 76 | 77 | object.each do |attribute, value| 78 | child_serialized[:attributes][attribute.to_sym] = value 79 | end 80 | 81 | child_serialized 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /app/services/forest_liana/base_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class BaseGetter 3 | def get_collection(collection_name) 4 | ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name } 5 | end 6 | 7 | def get_resource 8 | @resource.instance_methods.include?(:really_destroyed?) ? @resource : @resource.unscoped 9 | end 10 | 11 | def includes_for_serialization 12 | includes_for_smart_belongs_to = @collection.fields_smart_belongs_to.map { |field| field[:field] } 13 | includes_for_smart_belongs_to &= @field_names_requested if @field_names_requested 14 | 15 | @includes.concat(includes_for_smart_belongs_to).map(&:to_s) 16 | end 17 | 18 | private 19 | 20 | def compute_includes 21 | @includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@resource) 22 | end 23 | 24 | def optimize_record_loading(resource, records) 25 | polymorphic, preload_loads = analyze_associations(resource) 26 | result = records.eager_load(@includes.uniq - preload_loads - polymorphic) 27 | 28 | result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7 29 | 30 | result 31 | end 32 | 33 | def analyze_associations(resource) 34 | polymorphic = [] 35 | preload_loads = @includes.uniq.select do |name| 36 | association = resource.reflect_on_association(name) 37 | if SchemaUtils.polymorphic?(association) 38 | polymorphic << association.name 39 | false 40 | else 41 | separate_database?(resource, association) 42 | end 43 | end + instance_dependent_associations(resource) 44 | 45 | [polymorphic, preload_loads] 46 | end 47 | 48 | def separate_database?(resource, association) 49 | target_model_connection = association.klass.connection 50 | target_model_database = target_model_connection.current_database if target_model_connection.respond_to? :current_database 51 | resource_connection = resource.connection 52 | resource_database = resource_connection.current_database if resource_connection.respond_to? :current_database 53 | 54 | target_model_database != resource_database 55 | end 56 | 57 | def instance_dependent_associations(resource) 58 | @includes.select do |association_name| 59 | resource.reflect_on_association(association_name)&.scope&.arity&.positive? 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/fixtures/has_one_field.yml: -------------------------------------------------------------------------------- 1 | has_one_field_1: 2 | id: 1 3 | checked: true 4 | status: 0 5 | 6 | has_one_field_2: 7 | id: 2 8 | checked: true 9 | status: 0 10 | 11 | has_one_field_3: 12 | id: 3 13 | checked: true 14 | status: 0 15 | 16 | has_one_field_4: 17 | id: 4 18 | checked: true 19 | status: 0 20 | 21 | has_one_field_5: 22 | id: 5 23 | checked: true 24 | status: 0 25 | 26 | has_one_field_6: 27 | id: 6 28 | checked: true 29 | status: 0 30 | 31 | has_one_field_7: 32 | id: 7 33 | checked: true 34 | status: 0 35 | 36 | has_one_field_8: 37 | id: 8 38 | checked: true 39 | status: 0 40 | 41 | has_one_field_9: 42 | id: 9 43 | checked: true 44 | status: 0 45 | 46 | has_one_field_10: 47 | id: 10 48 | checked: true 49 | status: 0 50 | 51 | has_one_field_11: 52 | id: 11 53 | checked: true 54 | status: 0 55 | 56 | has_one_field_12: 57 | id: 12 58 | checked: true 59 | status: 0 60 | 61 | has_one_field_13: 62 | id: 13 63 | checked: true 64 | status: 0 65 | 66 | has_one_field_14: 67 | id: 14 68 | checked: true 69 | status: 0 70 | 71 | has_one_field_15: 72 | id: 15 73 | checked: true 74 | status: 0 75 | 76 | has_one_field_16: 77 | id: 16 78 | checked: true 79 | status: 0 80 | 81 | has_one_field_17: 82 | id: 17 83 | checked: true 84 | status: 0 85 | 86 | has_one_field_18: 87 | id: 18 88 | checked: true 89 | status: 0 90 | 91 | has_one_field_19: 92 | id: 19 93 | checked: true 94 | status: 0 95 | 96 | has_one_field_20: 97 | id: 20 98 | checked: true 99 | status: 0 100 | 101 | has_one_field_21: 102 | id: 21 103 | checked: true 104 | status: 0 105 | 106 | has_one_field_22: 107 | id: 22 108 | checked: true 109 | status: 0 110 | 111 | has_one_field_23: 112 | id: 23 113 | checked: true 114 | status: 0 115 | 116 | has_one_field_24: 117 | id: 24 118 | checked: true 119 | status: 0 120 | 121 | has_one_field_25: 122 | id: 25 123 | checked: true 124 | status: 0 125 | 126 | has_one_field_26: 127 | id: 26 128 | checked: true 129 | status: 0 130 | 131 | has_one_field_27: 132 | id: 27 133 | checked: true 134 | status: 0 135 | 136 | has_one_field_28: 137 | id: 28 138 | checked: true 139 | status: 0 140 | 141 | has_one_field_29: 142 | id: 29 143 | checked: true 144 | status: 0 145 | 146 | has_one_field_30: 147 | id: 30 148 | checked: true 149 | status: 1 150 | -------------------------------------------------------------------------------- /app/models/forest_liana/model/collection.rb: -------------------------------------------------------------------------------- 1 | class ForestLiana::Model::Collection 2 | include ActiveModel::Validations 3 | include ActiveModel::Conversion 4 | include ActiveModel::Serialization 5 | extend ActiveModel::Naming 6 | 7 | attr_accessor :name, :fields, :actions, :segments, :only_for_relationships, 8 | :is_virtual, :is_read_only, :is_searchable, :icon, 9 | :integration, :pagination_type, :search_fields, 10 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore. 11 | :name_old 12 | 13 | def initialize(attributes = {}) 14 | attributes.each do |name, value| 15 | send("#{name}=", value) 16 | end 17 | 18 | init_properties_with_default 19 | end 20 | 21 | def init_properties_with_default 22 | @name_old ||= @name 23 | @is_virtual ||= false 24 | @icon ||= nil 25 | @is_read_only ||= false 26 | @is_searchable = true if @is_searchable.nil? 27 | @only_for_relationships ||= false 28 | @pagination_type ||= "page" 29 | @search_fields ||= nil 30 | @fields ||= [] 31 | @actions ||= [] 32 | @segments ||= [] 33 | 34 | @fields = @fields.map do |field| 35 | field[:type] = "String" unless field.key?(:type) 36 | field[:default_value] = nil unless field.key?(:default_value) 37 | field[:enums] = nil unless field.key?(:enums) 38 | field[:integration] = nil unless field.key?(:integration) 39 | field[:is_filterable] = true unless field.key?(:is_filterable) 40 | field[:is_read_only] = false unless field.key?(:is_read_only) 41 | field[:is_required] = false unless field.key?(:is_required) 42 | field[:is_sortable] = true unless field.key?(:is_sortable) 43 | field[:is_virtual] = false unless field.key?(:is_virtual) 44 | field[:reference] = nil unless field.key?(:reference) 45 | field[:inverse_of] = nil unless field.key?(:inverse_of) 46 | field[:relationship] = nil unless field.key?(:relationship) 47 | field[:widget] = nil unless field.key?(:widget) 48 | field[:validations] = nil unless field.key?(:validations) 49 | field 50 | end 51 | end 52 | 53 | def persisted? 54 | false 55 | end 56 | 57 | def id 58 | name 59 | end 60 | 61 | def fields_smart_belongs_to 62 | fields.select do |field| 63 | field[:'is_virtual'] && field[:type] == 'String' && !field[:reference].nil? 64 | end 65 | end 66 | 67 | def string_smart_fields_names 68 | fields 69 | .select { |field| field[:'is_virtual'] && field[:type] == 'String' } 70 | .map { |field| field[:field].to_s } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/services/forest_liana/smart_action_field_validator.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class SmartActionFieldValidator 3 | 4 | @@accepted_primitive_field_type = [ 5 | 'String', 6 | 'Number', 7 | 'Date', 8 | 'Boolean', 9 | 'File', 10 | 'Enum', 11 | 'Json', 12 | 'Dateonly', 13 | ] 14 | 15 | @@accepted_array_field_type = [ 16 | 'String', 17 | 'Number', 18 | 'Date', 19 | 'boolean', 20 | 'File', 21 | 'Enum', 22 | ] 23 | 24 | def self.validate_field(field, action_name) 25 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be defined") if !field || field[:field].nil? 26 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be a string.") if !field[:field].is_a?(String) 27 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The description attribute must be a string.") if field[:description] && !field[:description].is_a?(String) 28 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The enums attribute must be an array.") if field[:enums] && !field[:enums].is_a?(Array) 29 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The reference attribute must be a string.") if field[:reference] && !field[:reference].is_a?(String) 30 | 31 | is_type_valid = field[:type].is_a?(Array) ? 32 | @@accepted_array_field_type.include?(field[:type][0]) : 33 | @@accepted_primitive_field_type.include?(field[:type]) 34 | 35 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The type attribute must be a valid type. See the documentation for more information. https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields#available-field-options.") if !is_type_valid 36 | end 37 | 38 | def self.validate_field_change_hook(field, action_name, hooks) 39 | raise ForestLiana::Errors::SmartActionInvalidFieldHookError.new(action_name, field[:field], field[:hook]) if field[:hook] && !hooks.find{|hook| hook == field[:hook]} 40 | end 41 | 42 | def self.validate_smart_action_fields(fields, action_name, change_hooks) 43 | fields.map{|field| 44 | self.validate_field(field.symbolize_keys, action_name) 45 | self.validate_field_change_hook(field.symbolize_keys, action_name, change_hooks) if change_hooks 46 | } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/config/initializers/logger_spec.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | describe Logger do 3 | describe 'self.log' do 4 | describe 'with a logger overload' do 5 | it 'should return the given logger' do 6 | logger = ActiveSupport::Logger.new($stdout) 7 | logger.formatter = proc do |severity, datetime, progname, msg| 8 | {:message => msg}.to_json 9 | end 10 | ForestLiana.logger = logger 11 | 12 | expect(Logger.log.is_a?(ActiveSupport::Logger)).to be_truthy 13 | expect { Logger.log.error "[error] override logger" }.to output({:message => "[error] override logger"}.to_json).to_stdout_from_any_process 14 | expect { Logger.log.info "[info] override logger" }.to output({:message => "[info] override logger"}.to_json).to_stdout_from_any_process 15 | end 16 | end 17 | 18 | describe 'with no logger overload' do 19 | it 'should return an instance of ::Logger' do 20 | ForestLiana.logger = nil 21 | 22 | expect(Logger.log.is_a?(::Logger)).to be_truthy 23 | # RegExp is used to check for the forestadmin logger format 24 | expect { Logger.log.error "[error] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (\+|\-)(\d{4})\] Forest .* \[error\]/).to_stdout_from_any_process 25 | expect { Logger.log.info "[info] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (\+|\-)(\d{4})\] Forest .* \[info\]/).to_stdout_from_any_process 26 | end 27 | end 28 | end 29 | end 30 | 31 | describe Reporter do 32 | describe 'self.reporter' do 33 | describe 'with a reporter provided' do 34 | it 'should report the error' do 35 | class SampleReporter 36 | def report(error) 37 | end 38 | end 39 | 40 | spier = spy('sampleReporter') 41 | 42 | ForestLiana.reporter = spier 43 | FOREST_REPORTER.report(Exception.new "sample error") 44 | 45 | expect(spier).to have_received(:report) 46 | ForestLiana.reporter = nil 47 | end 48 | end 49 | 50 | describe 'without any reporter provided' do 51 | it 'should not report the error' do 52 | class ErrorReporter 53 | def report(error) 54 | expect(false).to be_truthy 55 | end 56 | end 57 | 58 | spier = spy('errorReporter') 59 | 60 | FOREST_REPORTER.report(Exception.new "sample error") 61 | 62 | expect(spier).not_to have_received(:export) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/services/forest_liana/pie_stat_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class PieStatGetter < StatGetter 3 | attr_accessor :record 4 | 5 | def perform 6 | if @params[:groupByFieldName] 7 | timezone_offset = @params[:timezone].to_i 8 | resource = optimize_record_loading(@resource, get_resource) 9 | 10 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables']) 11 | 12 | unless filters.blank? 13 | resource = FiltersParser.new(filters, resource, @params[:timezone], @params).apply_filters 14 | end 15 | 16 | result = resource 17 | .group(groupByFieldName) 18 | .order(order) 19 | .send(@params[:aggregator].downcase, @params[:aggregateFieldName]) 20 | .map do |key, value| 21 | # NOTICE: Display the enum name instead of an integer if it is an 22 | # "Enum" field type on old Rails version (before Rails 23 | # 5.1.3). 24 | if @resource.respond_to?(:defined_enums) && 25 | @resource.defined_enums.has_key?(@params[:groupByFieldName]) && 26 | key.is_a?(Integer) 27 | key = @resource.defined_enums[@params[:groupByFieldName]].invert[key] 28 | elsif @resource.columns_hash[@params[:groupByFieldName]] && 29 | @resource.columns_hash[@params[:groupByFieldName]].type == :datetime 30 | key = (key + timezone_offset.hours).strftime('%d/%m/%Y %T') 31 | end 32 | 33 | { key: key, value: value } 34 | end 35 | 36 | @record = Model::Stat.new(value: result) 37 | end 38 | end 39 | 40 | def groupByFieldName 41 | if @params[:groupByFieldName].include? ':' 42 | association, field = @params[:groupByFieldName].split ':' 43 | resource = @resource.reflect_on_association(association.to_sym) 44 | "#{resource.table_name}.#{field}" 45 | else 46 | "#{@resource.table_name}.#{@params[:groupByFieldName]}" 47 | end 48 | end 49 | 50 | def order 51 | order = 'DESC' 52 | 53 | # NOTICE: The generated alias for a count is "count_all", for a sum the 54 | # alias looks like "sum_#{aggregateFieldName}" 55 | if @params[:aggregator].downcase == 'sum' 56 | field = @params[:aggregateFieldName].downcase 57 | else 58 | # `count_id` is required only for rails v5 59 | field = Rails::VERSION::MAJOR == 5 || @includes.size > 0 ? 'id' : 'all' 60 | end 61 | "#{@params[:aggregator].downcase}_#{field} #{order}" 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/services/forest_liana/mixpanel_last_events_getter.rb: -------------------------------------------------------------------------------- 1 | module ForestLiana 2 | class MixpanelLastEventsGetter < IntegrationBaseGetter 3 | attr_accessor :record 4 | 5 | def initialize(params) 6 | @params = params 7 | api_secret = ForestLiana.integrations[:mixpanel][:api_secret] 8 | @custom_properties = ForestLiana.integrations[:mixpanel][:custom_properties] 9 | @mixpanel = Mixpanel::Client.new(api_secret: api_secret) 10 | end 11 | 12 | def perform(field_name, field_value) 13 | result = @mixpanel.request( 14 | 'jql', 15 | script: "function main() { 16 | return People().filter(function (user) { 17 | return user.properties.$email == '#{field_value}'; 18 | }); 19 | }" 20 | ) 21 | 22 | if result.length == 0 23 | @records = [] 24 | return 25 | end 26 | 27 | from_date = (DateTime.now - 60.days).strftime("%Y-%m-%d") 28 | to_date = DateTime.now.strftime("%Y-%m-%d") 29 | distinct_id = result[0]['distinct_id'] 30 | 31 | result = @mixpanel.request( 32 | 'stream/query', 33 | from_date: from_date, 34 | to_date: to_date, 35 | distinct_ids: [distinct_id], 36 | limit: 100 37 | ) 38 | 39 | if result['status'] != 'ok' 40 | FOREST_LOGGER.error "Cannot retrieve the Mixpanel last events" 41 | @records = [] 42 | return 43 | end 44 | 45 | if result.length == 0 46 | @records = [] 47 | return 48 | end 49 | 50 | @records = process_result(result['results']['events']) 51 | end 52 | 53 | def process_result(events) 54 | events.reverse.map { |event| 55 | properties = event['properties'] 56 | 57 | new_event = { 58 | 'id' => SecureRandom.uuid, 59 | 'event' => event['event'], 60 | 'city' => properties['$city'], 61 | 'region' => properties['$region'], 62 | 'timezone' => properties['$timezone'], 63 | 'os' => properties['$os'], 64 | 'osVersion' => properties['$os_version'], 65 | 'country' => properties['mp_country_code'], 66 | 'browser' => properties['browser'], 67 | } 68 | 69 | time = properties['time'].to_s 70 | new_event['date'] = DateTime.strptime(time,'%s').strftime("%Y-%m-%dT%H:%M:%S%z") 71 | 72 | custom_attributes = event['properties'].select { |key, _| @custom_properties.include? key } 73 | new_event = new_event.merge(custom_attributes) 74 | 75 | ForestLiana::MixpanelEvent.new(new_event) 76 | } 77 | end 78 | 79 | def records 80 | @records 81 | end 82 | 83 | def count 84 | @records.count 85 | end 86 | end 87 | end 88 | --------------------------------------------------------------------------------