820 lines
27 KiB
Ruby
820 lines
27 KiB
Ruby
require 'active_record'
|
|
require 'cartodb-common'
|
|
require_relative 'user_service'
|
|
require_relative 'user_db_service'
|
|
require_relative 'synchronization_oauth'
|
|
require_relative '../../helpers/data_services_metrics_helper'
|
|
require_dependency 'carto/helpers/auth_token_generator'
|
|
require_dependency 'carto/helpers/has_connector_configuration'
|
|
require_dependency 'carto/helpers/batch_queries_statement_timeout'
|
|
require_dependency 'carto/helpers/billing_cycle'
|
|
|
|
# TODO: This probably has to be moved as the service of the proper User Model
|
|
class Carto::User < ActiveRecord::Base
|
|
extend Forwardable
|
|
include DataServicesMetricsHelper
|
|
include Carto::AuthTokenGenerator
|
|
include Carto::HasConnectorConfiguration
|
|
include Carto::BatchQueriesStatementTimeout
|
|
include Carto::BillingCycle
|
|
|
|
GEOCODING_BLOCK_SIZE = 1000
|
|
HERE_ISOLINES_BLOCK_SIZE = 1000
|
|
OBS_SNAPSHOT_BLOCK_SIZE = 1000
|
|
OBS_GENERAL_BLOCK_SIZE = 1000
|
|
MAPZEN_ROUTING_BLOCK_SIZE = 1000
|
|
|
|
STATE_ACTIVE = 'active'.freeze
|
|
STATE_LOCKED = 'locked'.freeze
|
|
|
|
# Make sure the following date is after Jan 29, 2015,
|
|
# which is the date where a message to accept the Terms and
|
|
# conditions and the Privacy policy was included in the Signup page.
|
|
# See https://github.com/CartoDB/cartodb-central/commit/3627da19f071c8fdd1604ddc03fb21ab8a6dff9f
|
|
FULLSTORY_ENABLED_MIN_DATE = Date.new(2017, 1, 1)
|
|
FULLSTORY_SUPPORTED_PLANS = ['FREE', 'PERSONAL30', 'Individual'].freeze
|
|
|
|
MAGELLAN_TRIAL_DAYS = 15
|
|
PERSONAL30_TRIAL_DAYS = 30
|
|
INDIVIDUAL_TRIAL_DAYS = 14
|
|
TRIAL_PLANS = ['personal30', 'individual'].freeze
|
|
|
|
# INFO: select filter is done for security and performance reasons. Add new columns if needed.
|
|
DEFAULT_SELECT = "users.email, users.username, users.admin, users.organization_id, users.id, users.avatar_url," \
|
|
"users.api_key, users.database_schema, users.database_name, users.name, users.location," \
|
|
"users.disqus_shortname, users.account_type, users.twitter_username, users.google_maps_key, " \
|
|
"users.viewer, users.quota_in_bytes, users.database_host, users.crypted_password, " \
|
|
"users.builder_enabled, users.private_tables_enabled, users.private_maps_enabled, " \
|
|
"users.org_admin, users.last_name, users.google_maps_private_key, users.website, " \
|
|
"users.description, users.available_for_hire, users.frontend_version, users.asset_host, "\
|
|
"users.no_map_logo, users.industry, users.company, users.phone, users.job_role, "\
|
|
"users.public_map_quota, users.maintenance_mode, users.company_employees, users.use_case".freeze
|
|
|
|
has_many :tables, class_name: Carto::UserTable, inverse_of: :user
|
|
has_many :visualizations, inverse_of: :user
|
|
has_many :maps, inverse_of: :user
|
|
has_many :layers_user
|
|
has_many :layers, through: :layers_user, after_add: Proc.new { |user, layer| layer.set_default_order(user) }
|
|
|
|
belongs_to :organization, inverse_of: :users
|
|
belongs_to :rate_limit
|
|
has_one :owned_organization, class_name: Carto::Organization, inverse_of: :owner, foreign_key: :owner_id
|
|
has_one :static_notifications, class_name: Carto::UserNotification, inverse_of: :user
|
|
|
|
has_many :feature_flags_user, dependent: :destroy, foreign_key: :user_id, inverse_of: :user
|
|
has_many :feature_flags, through: :feature_flags_user
|
|
has_many :assets, inverse_of: :user
|
|
has_many :data_imports, inverse_of: :user
|
|
has_many :geocodings, inverse_of: :user
|
|
has_many :synchronization_oauths, class_name: Carto::SynchronizationOauth, inverse_of: :user, dependent: :destroy
|
|
has_many :search_tweets, inverse_of: :user
|
|
has_many :synchronizations, inverse_of: :user
|
|
has_many :tags, inverse_of: :user
|
|
has_many :permissions, inverse_of: :owner, foreign_key: :owner_id
|
|
has_many :connector_configurations, inverse_of: :user, dependent: :destroy
|
|
|
|
has_many :client_applications, class_name: Carto::ClientApplication
|
|
has_many :oauth_tokens, class_name: Carto::OauthToken
|
|
|
|
has_many :users_group, dependent: :destroy, class_name: Carto::UsersGroup
|
|
has_many :groups, through: :users_group
|
|
|
|
has_many :received_notifications, inverse_of: :user
|
|
|
|
has_many :api_keys, inverse_of: :user
|
|
has_many :user_multifactor_auths, inverse_of: :user, class_name: Carto::UserMultifactorAuth
|
|
|
|
has_many :oauth_apps, inverse_of: :user, dependent: :destroy
|
|
has_many :oauth_app_users, inverse_of: :user, dependent: :destroy
|
|
has_many :granted_oauth_apps, through: :oauth_app_users, class_name: Carto::OauthApp, source: 'oauth_app'
|
|
|
|
delegate [
|
|
:database_username, :database_password, :in_database,
|
|
:db_size_in_bytes, :get_api_calls, :table_count, :public_visualization_count, :all_visualization_count,
|
|
:visualization_count, :owned_visualization_count, :twitter_imports_count,
|
|
:link_privacy_visualization_count, :password_privacy_visualization_count, :public_privacy_visualization_count
|
|
] => :service
|
|
|
|
attr_reader :password
|
|
|
|
# TODO: From sequel, can be removed once finished
|
|
alias_method :maps_dataset, :maps
|
|
alias_method :layers_dataset, :layers
|
|
alias_method :assets_dataset, :assets
|
|
alias_method :data_imports_dataset, :data_imports
|
|
alias_method :geocodings_dataset, :geocodings
|
|
|
|
before_create :set_database_host
|
|
before_create :generate_api_key
|
|
|
|
after_save { reset_password_rate_limit if crypted_password_changed? }
|
|
|
|
after_destroy { rate_limit.destroy_completely(self) if rate_limit }
|
|
after_destroy :invalidate_varnish_cache
|
|
|
|
LOGIN_NOT_RATE_LIMITED = -1
|
|
|
|
MULTIFACTOR_AUTHENTICATION_ENABLED = 'enabled'.freeze
|
|
MULTIFACTOR_AUTHENTICATION_DISABLED = 'disabled'.freeze
|
|
MULTIFACTOR_AUTHENTICATION_NEEDS_SETUP = 'setup'.freeze
|
|
|
|
include ::VarnishCacheHandler
|
|
|
|
# Auto creates notifications on first access
|
|
def static_notifications_with_creation
|
|
static_notifications_without_creation || build_static_notifications(user: self, notifications: {})
|
|
end
|
|
alias_method_chain :static_notifications, :creation
|
|
|
|
def name_or_username
|
|
name.present? || last_name.present? ? [name, last_name].select(&:present?).join(' ') : username
|
|
end
|
|
|
|
def password_validator
|
|
if organization.try(:strong_passwords_enabled)
|
|
Carto::PasswordValidator.new(Carto::StrongPasswordStrategy.new)
|
|
else
|
|
Carto::PasswordValidator.new(Carto::StandardPasswordStrategy.new)
|
|
end
|
|
end
|
|
|
|
def password=(value)
|
|
return if !value.nil? && password_validator.validate(value, value, self).any?
|
|
|
|
@password = value
|
|
self.crypted_password = Carto::Common::EncryptionService.encrypt(password: value,
|
|
secret: Cartodb.config[:password_secret])
|
|
end
|
|
|
|
def reset_password_rate_limit
|
|
$users_metadata.DEL rate_limit_password_key if password_rate_limit_configured?
|
|
end
|
|
|
|
def rate_limit_password_key
|
|
"limits:password:#{username}"
|
|
end
|
|
|
|
def password_login_attempt
|
|
return LOGIN_NOT_RATE_LIMITED unless password_rate_limit_configured?
|
|
|
|
rate_limit = $users_metadata.call('CL.THROTTLE', rate_limit_password_key, @max_burst, @count, @period)
|
|
|
|
# it returns the number of seconds until the user should retry
|
|
# -1 means the action was allowed
|
|
# see https://github.com/brandur/redis-cell#response
|
|
rate_limit[3]
|
|
end
|
|
|
|
def password_confirmation=(password_confirmation)
|
|
# TODO: Implement
|
|
end
|
|
|
|
def default_avatar
|
|
"cartodb.s3.amazonaws.com/static/public_dashboard_default_avatar.png"
|
|
end
|
|
|
|
def feature_flag_names
|
|
@feature_flag_names ||= (feature_flags_user.map do |ff|
|
|
ff.feature_flag.name
|
|
end +
|
|
FeatureFlag.where(restricted: false).map(&:name)).uniq.sort
|
|
end
|
|
|
|
# TODO: Revisit methods below to delegate to the service, many look like not proper of the model itself
|
|
|
|
def service
|
|
@service ||= Carto::UserService.new(self)
|
|
end
|
|
|
|
def db_service
|
|
@db_service ||= Carto::UserDBService.new(self)
|
|
end
|
|
|
|
# +--------+---------+------+
|
|
# valid_privacy logic | Public | Private | Link |
|
|
# +-------------------------+--------+---------+------+
|
|
# | private_tables_enabled | T | T | T |
|
|
# | !private_tables_enabled | T | F | F |
|
|
# +-------------------------+--------+---------+------+
|
|
#
|
|
def valid_privacy?(privacy)
|
|
private_tables_enabled || privacy == Carto::UserTable::PRIVACY_PUBLIC
|
|
end
|
|
|
|
def default_dataset_privacy
|
|
Carto::UserTable::PRIVACY_VALUES_TO_TEXTS[default_table_privacy]
|
|
end
|
|
|
|
def default_table_privacy
|
|
private_tables_enabled ? Carto::UserTable::PRIVACY_PRIVATE : Carto::UserTable::PRIVACY_PUBLIC
|
|
end
|
|
|
|
# @return String public user url, which is also the base url for a given user
|
|
def public_url(subdomain_override = nil, protocol_override = nil)
|
|
base_subdomain = subdomain_override.nil? ? subdomain : subdomain_override
|
|
CartoDB.base_url(base_subdomain, CartoDB.organization_username(self), protocol_override)
|
|
end
|
|
|
|
def subdomain
|
|
if CartoDB.subdomainless_urls?
|
|
username
|
|
else
|
|
organization.nil? ? username : organization.name
|
|
end
|
|
end
|
|
|
|
def feature_flags_list
|
|
@feature_flag_names ||= (feature_flags_user
|
|
.map { |ff| ff.feature_flag.name } + FeatureFlag.where(restricted: false)
|
|
.map(&:name)).uniq.sort
|
|
end
|
|
|
|
def has_feature_flag?(feature_flag_name)
|
|
feature_flags_list.present? && feature_flags_list.include?(feature_flag_name)
|
|
end
|
|
|
|
def has_organization?
|
|
!organization_id.nil?
|
|
end
|
|
|
|
def avatar
|
|
avatar_url.nil? ? "//#{default_avatar}" : avatar_url
|
|
end
|
|
|
|
def remove_logo?
|
|
has_organization? ? organization.no_map_logo? : no_map_logo?
|
|
end
|
|
|
|
def sql_safe_database_schema
|
|
database_schema.include?('-') ? "\"#{database_schema}\"" : database_schema
|
|
end
|
|
|
|
def database_public_username
|
|
database_schema == CartoDB::DEFAULT_DB_SCHEMA ? CartoDB::PUBLIC_DB_USER : "cartodb_publicuser_#{id}"
|
|
end
|
|
|
|
# returns google maps api key. If the user is in an organization and
|
|
# that organization has api key it's used
|
|
def google_maps_api_key
|
|
organization.try(:google_maps_key).blank? ? google_maps_key : organization.google_maps_key
|
|
end
|
|
|
|
def twitter_datasource_enabled
|
|
(read_attribute(:twitter_datasource_enabled) || organization.try(&:twitter_datasource_enabled)) && twitter_configured?
|
|
end
|
|
|
|
def twitter_configured?
|
|
# DatasourcesFactory.config_for takes configuration from organization if user is an organization user
|
|
CartoDB::Datasources::DatasourcesFactory.customized_config?(Search::Twitter::DATASOURCE_NAME, self)
|
|
end
|
|
|
|
# TODO: this is the correct name for what's stored in the model, refactor changing that name
|
|
alias_method :google_maps_query_string, :google_maps_api_key
|
|
|
|
# Returns the google maps private key. If the user is in an organization and
|
|
# that organization has a private key, the org's private key is returned.
|
|
def google_maps_private_key
|
|
if organization.try(:google_maps_private_key).blank?
|
|
read_attribute(:google_maps_private_key)
|
|
else
|
|
organization.google_maps_private_key
|
|
end
|
|
end
|
|
|
|
def google_maps_geocoder_enabled?
|
|
google_maps_private_key.present? && google_maps_client_id.present?
|
|
end
|
|
|
|
def google_maps_client_id
|
|
Rack::Utils.parse_nested_query(google_maps_query_string)['client'] if google_maps_query_string
|
|
end
|
|
|
|
# returns a list of basemaps enabled for the user
|
|
def basemaps
|
|
(Cartodb.config[:basemaps] || []).select { |group| group != 'GMaps' || google_maps_enabled? }
|
|
end
|
|
|
|
def google_maps_enabled?
|
|
google_maps_query_string.present?
|
|
end
|
|
|
|
# return the default basemap based on the default setting. If default attribute is not set, first basemaps is returned
|
|
# it only takes into account basemaps enabled for that user
|
|
def default_basemap
|
|
default = if google_maps_enabled? && basemaps['GMaps'].present?
|
|
['GMaps', basemaps['GMaps']]
|
|
else
|
|
basemaps.find { |_, group_basemaps| group_basemaps.find { |_, attr| attr['default'] } }
|
|
end
|
|
default ||= basemaps.first
|
|
# return only the attributes
|
|
default[1].first[1]
|
|
end
|
|
|
|
def remaining_geocoding_quota(options = {})
|
|
remaining = if organization.present?
|
|
organization.remaining_geocoding_quota(options)
|
|
else
|
|
geocoding_quota - get_geocoding_calls(options)
|
|
end
|
|
(remaining > 0 ? remaining : 0)
|
|
end
|
|
|
|
def remaining_here_isolines_quota(options = {})
|
|
remaining = if organization.present?
|
|
organization.remaining_here_isolines_quota(options)
|
|
else
|
|
here_isolines_quota - get_here_isolines_calls(options)
|
|
end
|
|
(remaining > 0 ? remaining : 0)
|
|
end
|
|
|
|
def remaining_obs_snapshot_quota(options = {})
|
|
remaining = if organization.present?
|
|
organization.remaining_obs_snapshot_quota(options)
|
|
else
|
|
obs_snapshot_quota - get_obs_snapshot_calls(options)
|
|
end
|
|
(remaining > 0 ? remaining : 0)
|
|
end
|
|
|
|
def remaining_obs_general_quota(options = {})
|
|
remaining = if organization.present?
|
|
organization.remaining_obs_general_quota(options)
|
|
else
|
|
obs_general_quota - get_obs_general_calls(options)
|
|
end
|
|
(remaining > 0 ? remaining : 0)
|
|
end
|
|
|
|
def remaining_mapzen_routing_quota(options = {})
|
|
remaining = if organization.present?
|
|
organization.remaining_mapzen_routing_quota(options)
|
|
else
|
|
mapzen_routing_quota.to_i - get_mapzen_routing_calls(options)
|
|
end
|
|
(remaining > 0 ? remaining : 0)
|
|
end
|
|
|
|
def oauth_for_service(service)
|
|
synchronization_oauths.where(service: service).first
|
|
end
|
|
|
|
# INFO: don't use, use CartoDB::OAuths#add instead
|
|
def add_oauth(service, token)
|
|
# INFO: this should be the right way, but there's a problem with pgbouncer:
|
|
# ActiveRecord::StatementInvalid: PG::Error: ERROR: prepared statement "a1" does not exist
|
|
# synchronization_oauths.create(
|
|
# service: service,
|
|
# token: token
|
|
# )
|
|
# INFO: even this fails eventually, th the same error. See https://github.com/CartoDB/cartodb/issues/4003
|
|
synchronization_oauth = Carto::SynchronizationOauth.new(
|
|
user_id: id,
|
|
service: service,
|
|
token: token
|
|
)
|
|
synchronization_oauth.save
|
|
synchronization_oauths.append(synchronization_oauth)
|
|
synchronization_oauth
|
|
end
|
|
|
|
def get_geocoding_calls(options = {})
|
|
date_from, date_to, orgwise = ds_metrics_parameters_from_options(options)
|
|
get_user_geocoding_data(self, date_from, date_to, orgwise)
|
|
end
|
|
|
|
def get_here_isolines_calls(options = {})
|
|
date_from, date_to, orgwise = ds_metrics_parameters_from_options(options)
|
|
get_user_here_isolines_data(self, date_from, date_to, orgwise)
|
|
end
|
|
|
|
def get_obs_snapshot_calls(options = {})
|
|
date_from, date_to, orgwise = ds_metrics_parameters_from_options(options)
|
|
get_user_obs_snapshot_data(self, date_from, date_to, orgwise)
|
|
end
|
|
|
|
def get_obs_general_calls(options = {})
|
|
date_from, date_to, orgwise = ds_metrics_parameters_from_options(options)
|
|
get_user_obs_general_data(self, date_from, date_to, orgwise)
|
|
end
|
|
|
|
def get_mapzen_routing_calls(options = {})
|
|
date_from, date_to, orgwise = ds_metrics_parameters_from_options(options)
|
|
get_user_mapzen_routing_data(self, date_from, date_to, orgwise)
|
|
end
|
|
|
|
# TODO: Remove unused param `use_total`
|
|
def remaining_quota(_use_total = false, db_size = service.db_size_in_bytes)
|
|
return nil unless db_size
|
|
|
|
quota_in_bytes - db_size
|
|
end
|
|
|
|
# can be nil table quotas
|
|
def remaining_table_quota
|
|
if table_quota.present?
|
|
remaining = table_quota - service.table_count
|
|
remaining < 0 ? 0 : remaining
|
|
end
|
|
end
|
|
|
|
def organization_user?
|
|
organization.present?
|
|
end
|
|
|
|
def belongs_to_organization?(organization)
|
|
organization_user? && !organization.nil? && organization_id == organization.id
|
|
end
|
|
|
|
def soft_geocoding_limit?
|
|
Carto::AccountType.new.soft_geocoding_limit?(self)
|
|
end
|
|
alias_method :soft_geocoding_limit, :soft_geocoding_limit?
|
|
|
|
def hard_geocoding_limit?
|
|
!soft_geocoding_limit?
|
|
end
|
|
alias_method :hard_geocoding_limit, :hard_geocoding_limit?
|
|
|
|
def soft_here_isolines_limit?
|
|
Carto::AccountType.new.soft_here_isolines_limit?(self)
|
|
end
|
|
alias_method :soft_here_isolines_limit, :soft_here_isolines_limit?
|
|
|
|
def hard_here_isolines_limit?
|
|
!soft_here_isolines_limit?
|
|
end
|
|
alias_method :hard_here_isolines_limit, :hard_here_isolines_limit?
|
|
|
|
def soft_obs_snapshot_limit?
|
|
Carto::AccountType.new.soft_obs_snapshot_limit?(self)
|
|
end
|
|
alias_method :soft_obs_snapshot_limit, :soft_obs_snapshot_limit?
|
|
|
|
def hard_obs_snapshot_limit?
|
|
!soft_obs_snapshot_limit?
|
|
end
|
|
alias_method :hard_obs_snapshot_limit, :hard_obs_snapshot_limit?
|
|
|
|
def soft_obs_general_limit?
|
|
Carto::AccountType.new.soft_obs_general_limit?(self)
|
|
end
|
|
alias_method :soft_obs_general_limit, :soft_obs_general_limit?
|
|
|
|
def hard_obs_general_limit?
|
|
!soft_obs_general_limit?
|
|
end
|
|
alias_method :hard_obs_general_limit, :hard_obs_general_limit?
|
|
|
|
def soft_twitter_datasource_limit?
|
|
soft_twitter_datasource_limit == true
|
|
end
|
|
|
|
def hard_twitter_datasource_limit?
|
|
!soft_twitter_datasource_limit?
|
|
end
|
|
alias_method :hard_twitter_datasource_limit, :hard_twitter_datasource_limit?
|
|
|
|
def soft_mapzen_routing_limit?
|
|
Carto::AccountType.new.soft_mapzen_routing_limit?(self)
|
|
end
|
|
alias_method :soft_mapzen_routing_limit, :soft_mapzen_routing_limit?
|
|
|
|
def hard_mapzen_routing_limit?
|
|
!soft_mapzen_routing_limit?
|
|
end
|
|
alias_method :hard_mapzen_routing_limit, :hard_mapzen_routing_limit?
|
|
def trial_ends_at
|
|
if account_type.to_s.casecmp('magellan').zero? && upgraded_at && upgraded_at + 15.days > Date.today
|
|
upgraded_at + MAGELLAN_TRIAL_DAYS.days
|
|
elsif account_type.to_s.casecmp('personal30').zero?
|
|
created_at + PERSONAL30_TRIAL_DAYS.days
|
|
elsif account_type.to_s.casecmp('individual').zero?
|
|
created_at + INDIVIDUAL_TRIAL_DAYS.days
|
|
end
|
|
end
|
|
|
|
def remaining_days_deletion
|
|
return nil unless state == STATE_LOCKED
|
|
begin
|
|
deletion_date = Cartodb::Central.new.get_user(username).fetch('scheduled_deletion_date', nil)
|
|
return nil unless deletion_date
|
|
(deletion_date.to_date - Date.today).to_i
|
|
rescue StandardError => e
|
|
CartoDB::Logger.warning(exception: e, message: 'Something went wrong calculating the number of remaining days for account deletion')
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def viewable_by?(viewer)
|
|
id == viewer.id || organization.try(:admin?, viewer)
|
|
end
|
|
|
|
def editable_by?(user)
|
|
id == user.id || user.belongs_to_organization?(organization) && (user.organization_owner? || !organization_admin?)
|
|
end
|
|
|
|
# Some operations, such as user deletion, won't ask for password confirmation if password is not set (because of Google sign in, for example)
|
|
def needs_password_confirmation?
|
|
(!oauth_signin? || !last_password_change_date.nil?) &&
|
|
!created_with_http_authentication? &&
|
|
!organization.try(:auth_saml_enabled?)
|
|
end
|
|
|
|
def validate_old_password(old_password)
|
|
return true unless needs_password_confirmation?
|
|
|
|
Carto::Common::EncryptionService.verify(password: old_password, secure_password: crypted_password,
|
|
secret: Cartodb.config[:password_secret])
|
|
end
|
|
|
|
def valid_password_confirmation(password)
|
|
valid = validate_old_password(password)
|
|
errors.add(:password, 'Confirmation password sent does not match your current password') unless valid
|
|
valid
|
|
end
|
|
|
|
def valid_password?(key, value, confirmation_value)
|
|
password_validator.validate(value, confirmation_value, self).each { |e| errors.add(key, e) }
|
|
validate_password_not_in_use(nil, value, key)
|
|
|
|
errors[key].empty?
|
|
end
|
|
|
|
def validate_password_not_in_use(old_password = nil, new_password = nil, key = :new_password)
|
|
if password_in_use?(old_password, new_password)
|
|
errors.add(key, 'New password cannot be the same as old password')
|
|
end
|
|
errors[key].empty?
|
|
end
|
|
|
|
def password_in_use?(old_password = nil, new_password = nil)
|
|
return false if new_record?
|
|
return old_password == new_password if old_password
|
|
|
|
Carto::Common::EncryptionService.verify(password: new_password, secure_password: crypted_password_was,
|
|
secret: Cartodb.config[:password_secret])
|
|
end
|
|
|
|
alias_method :should_display_old_password?, :needs_password_confirmation?
|
|
alias_method :password_set?, :needs_password_confirmation?
|
|
|
|
def oauth_signin?
|
|
google_sign_in || github_user_id.present?
|
|
end
|
|
|
|
def created_with_http_authentication?
|
|
Carto::UserCreation.http_authentication.find_by_user_id(id).present?
|
|
end
|
|
|
|
def organization_owner?
|
|
organization && organization.owner_id == id
|
|
end
|
|
|
|
def organization_admin?
|
|
organization_user? && (organization_owner? || org_admin)
|
|
end
|
|
|
|
def mobile_sdk_enabled?
|
|
mobile_max_open_users > 0 || mobile_max_private_users > 0
|
|
end
|
|
|
|
def get_auth_tokens
|
|
tokens = [get_auth_token]
|
|
|
|
if has_organization?
|
|
tokens << organization.get_auth_token
|
|
tokens += groups.map(&:get_auth_token)
|
|
end
|
|
|
|
tokens
|
|
end
|
|
|
|
def get_auth_token
|
|
# Circumvent DEFAULT_SELECT, didn't add auth_token there for sercurity (presenters, etc)
|
|
auth_token = Carto::User.select(:auth_token).find(id).auth_token
|
|
|
|
auth_token || generate_and_save_auth_token
|
|
end
|
|
|
|
def notifications_for_category(category)
|
|
static_notifications.notifications[category] || {}
|
|
end
|
|
|
|
def builder_enabled?
|
|
if has_organization? && builder_enabled.nil?
|
|
organization.builder_enabled
|
|
else
|
|
!!builder_enabled
|
|
end
|
|
end
|
|
|
|
def engine_enabled?
|
|
if has_organization? && engine_enabled.nil?
|
|
organization.engine_enabled
|
|
else
|
|
!!engine_enabled
|
|
end
|
|
end
|
|
|
|
def new_visualizations_version
|
|
builder_enabled? ? 3 : 2
|
|
end
|
|
|
|
def can_change_email?
|
|
(!google_sign_in || last_password_change_date.present?) && !Carto::Ldap::Manager.new.configuration_present?
|
|
end
|
|
|
|
def can_change_password?
|
|
!Carto::Ldap::Manager.new.configuration_present?
|
|
end
|
|
|
|
def view_dashboard
|
|
update_column(:dashboard_viewed_at, Time.now)
|
|
end
|
|
|
|
# Special url that goes to Central if active (for old dashboard only)
|
|
def account_url(request_protocol)
|
|
request_protocol + CartoDB.account_host + CartoDB.account_path + '/' + username if CartoDB.account_host
|
|
end
|
|
|
|
# Special url that goes to Central if active
|
|
def plan_url(request_protocol)
|
|
account_url(request_protocol) + '/plan'
|
|
end
|
|
|
|
def relevant_frontend_version
|
|
frontend_version || CartoDB::Application.frontend_version
|
|
end
|
|
|
|
def cant_be_deleted_reason
|
|
if organization_owner?
|
|
"You can't delete your account because you are admin of an organization"
|
|
elsif Carto::UserCreation.http_authentication.where(user_id: id).first.present?
|
|
"You can't delete your account because you are using HTTP Header Authentication"
|
|
end
|
|
end
|
|
|
|
# Gets the list of OAuth accounts the user has (currently only used for synchronization)
|
|
# @return CartoDB::OAuths
|
|
def oauths
|
|
@oauths ||= CartoDB::OAuths.new(self)
|
|
end
|
|
|
|
def get_oauth_services
|
|
datasources = CartoDB::Datasources::DatasourcesFactory.get_all_oauth_datasources
|
|
array = []
|
|
|
|
datasources.each do |serv|
|
|
obj ||= Hash.new
|
|
|
|
title = ::User::OAUTH_SERVICE_TITLES.fetch(serv, serv)
|
|
revoke_url = ::User::OAUTH_SERVICE_REVOKE_URLS.fetch(serv, nil)
|
|
enabled = case serv
|
|
when 'gdrive'
|
|
Cartodb.config[:oauth][serv]['client_id'].present?
|
|
when 'box'
|
|
Cartodb.config[:oauth][serv]['client_id'].present?
|
|
when 'dropbox'
|
|
Cartodb.config[:oauth]['dropbox']['app_key'].present?
|
|
when 'mailchimp'
|
|
Cartodb.config[:oauth]['mailchimp']['app_key'].present? && has_feature_flag?('mailchimp_import')
|
|
when 'instagram'
|
|
Cartodb.config[:oauth]['instagram']['app_key'].present? && has_feature_flag?('instagram_import')
|
|
else
|
|
true
|
|
end
|
|
|
|
if enabled
|
|
oauth = oauths.select(serv)
|
|
|
|
obj['name'] = serv
|
|
obj['title'] = title
|
|
obj['revoke_url'] = revoke_url
|
|
obj['connected'] = !oauth.nil? ? true : false
|
|
|
|
array.push(obj)
|
|
end
|
|
end
|
|
|
|
array
|
|
end
|
|
|
|
def account_url(request_protocol)
|
|
if CartoDB.account_host
|
|
request_protocol + CartoDB.account_host + CartoDB.account_path + '/' + username
|
|
end
|
|
end
|
|
|
|
# Special url that goes to Central if active
|
|
def plan_url(request_protocol)
|
|
account_url(request_protocol) + '/plan'
|
|
end
|
|
|
|
def update_payment_url(request_protocol)
|
|
account_url(request_protocol) + '/update_payment'
|
|
end
|
|
|
|
def active?
|
|
state == STATE_ACTIVE
|
|
end
|
|
|
|
def locked?
|
|
state == STATE_LOCKED
|
|
end
|
|
|
|
def fullstory_enabled?
|
|
FULLSTORY_SUPPORTED_PLANS.include?(account_type) && created_at > FULLSTORY_ENABLED_MIN_DATE
|
|
end
|
|
|
|
def password_expired?
|
|
return false unless password_expiration_in_d && password_set?
|
|
password_date + password_expiration_in_d.days.to_i < Time.now
|
|
end
|
|
|
|
def password_expiration_in_d
|
|
organization_user? ? organization.password_expiration_in_d : Cartodb.get_config(:passwords, 'expiration_in_d')
|
|
end
|
|
|
|
def password_date
|
|
last_password_change_date || created_at
|
|
end
|
|
|
|
def send_password_reset!
|
|
generate_token(:password_reset_token)
|
|
self.password_reset_sent_at = Time.zone.now
|
|
save!
|
|
|
|
Resque.enqueue(::Resque::UserJobs::Mail::PasswordReset, id)
|
|
end
|
|
|
|
def multifactor_authentication_configured?
|
|
user_multifactor_auths.any?
|
|
end
|
|
|
|
def active_multifactor_authentication
|
|
user_multifactor_auths.order(created_at: :desc).first
|
|
end
|
|
|
|
def multifactor_authentication_status
|
|
if user_multifactor_auths.setup.any?
|
|
MULTIFACTOR_AUTHENTICATION_NEEDS_SETUP
|
|
elsif user_multifactor_auths.enabled.any?
|
|
MULTIFACTOR_AUTHENTICATION_ENABLED
|
|
else
|
|
MULTIFACTOR_AUTHENTICATION_DISABLED
|
|
end
|
|
end
|
|
|
|
def remaining_trial_days
|
|
return 0 unless trial_ends_at
|
|
((trial_ends_at - Time.now) / 1.day).round
|
|
end
|
|
|
|
def trial_user?
|
|
TRIAL_PLANS.include?(account_type.to_s.downcase)
|
|
end
|
|
|
|
def get_database_roles
|
|
api_key_roles = api_keys.reject { |k| k.db_role =~ /^publicuser/ }.map(&:db_role)
|
|
oauth_app_owner_roles = api_keys.reject { |k| k.effective_ownership_role_name == nil }.map(&:effective_ownership_role_name)
|
|
(api_key_roles + oauth_app_owner_roles).uniq
|
|
end
|
|
|
|
private
|
|
|
|
def password_rate_limit_configured?
|
|
@max_burst ||= Cartodb.get_config(:passwords, 'rate_limit', 'max_burst')
|
|
@count ||= Cartodb.get_config(:passwords, 'rate_limit', 'count')
|
|
@period ||= Cartodb.get_config(:passwords, 'rate_limit', 'period')
|
|
|
|
[@max_burst, @count, @period].all?(&:present?)
|
|
end
|
|
|
|
def set_database_host
|
|
self.database_host ||= ::SequelRails.configuration.environment_for(Rails.env)['host']
|
|
end
|
|
|
|
def generate_api_key
|
|
self.api_key ||= make_token
|
|
end
|
|
|
|
def generate_token(column)
|
|
begin
|
|
self[column] = SecureRandom.urlsafe_base64
|
|
end while Carto::User.exists?(column => self[column])
|
|
end
|
|
|
|
def ds_metrics_parameters_from_options(options)
|
|
date_from = (options[:from] ? options[:from].to_date : last_billing_cycle)
|
|
date_to = (options[:to] ? options[:to].to_date : Date.today)
|
|
orgwise = options.fetch(:orgwise, true)
|
|
[date_from, date_to, orgwise]
|
|
end
|
|
|
|
def make_token
|
|
Carto::Common::EncryptionService.make_token
|
|
end
|
|
end
|