Skip to content

Commit

Permalink
Merge pull request #15595 from CartoDB/apps-based-on-kuviz
Browse files Browse the repository at this point in the history
deploying apps
  • Loading branch information
oleurud committed Apr 21, 2020
2 parents 23789eb + 09a4cb3 commit 40eeb61
Show file tree
Hide file tree
Showing 31 changed files with 1,078 additions and 34 deletions.
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ WORKING_SPECS_1 = \
spec/requests/carto/api/user_creations_controller_spec.rb \
spec/requests/carto/api/widgets_controller_spec.rb \
spec/requests/carto/api/custom_visualizations_controller_spec.rb \
spec/requests/carto/api/apps_controller_spec.rb \
spec/requests/carto/builder/public/embeds_controller_spec.rb \
spec/requests/carto/builder/visualizations_controller_spec.rb \
spec/requests/carto/kuviz/visualizations_controller_spec.rb \
spec/requests/carto/app/visualizations_controller_spec.rb \
spec/requests/visualizations_controller_helper_spec.rb \
spec/requests/warden_spec.rb \
spec/models/map_spec.rb \
Expand Down Expand Up @@ -336,7 +338,7 @@ SPEC_HELPER_MIN_SPECS = \
spec/requests/carto/api/organization_assets_controller_spec.rb \
spec/lib/carto/assets/image_assets_service_spec.rb \
spec/lib/carto/assets/organization_image_assets_service_spec.rb \
spec/lib/carto/assets/kuviz_assets_service_spec.rb \
spec/lib/carto/assets/html_assets_service_spec.rb \
spec/lib/carto/storage_options/local_spec.rb \
spec/lib/carto/visualization_invalidation_service_spec.rb \
spec/lib/tasks/layers_rake_spec.rb \
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Development
- Add Kepler.gl maps to the Maps section in the Dashboard's Home page ([#15487](https://github.com/CartoDB/cartodb/issues/15487))
- Request connector flow with all the states on the same screen ([#15515](https://github.com/CartoDB/cartodb/issues/15515))
- Hooks to override org settings for gear plugin ([#15126](https://github.com/CartoDB/cartodb/pull/15126))
- New app visualization type and endpoints for deploying apps [#15595](https://github.com/CartoDB/cartodb/pull/15595)

### Bug fixes / enhancements
- Fixes bug in CartoDB Central communication ([#15606](https://github.com/CartoDB/cartodb/pull/15606))
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/admin/visualizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Admin::VisualizationsController < Admin::AdminController

before_filter :resolve_visualization_and_table_if_not_cached, only: [:embed_map]
before_filter :redirect_to_kuviz_if_needed, only: [:embed_map]
before_filter :redirect_to_app_if_needed, only: [:embed_map]
before_filter :redirect_to_builder_embed_if_v3, only: [:embed_map, :show_organization_public_map,
:show_organization_embed_map, :show_protected_public_map,
:show_protected_embed_map,
Expand Down Expand Up @@ -672,6 +673,10 @@ def redirect_to_kuviz_if_needed
redirect_to(CartoDB.url(self, 'kuviz_show', params: { id: @visualization.id })) if @visualization&.kuviz?
end

def redirect_to_app_if_needed
redirect_to(CartoDB.url(self, 'app_show', params: { id: @visualization.id })) if @visualization&.app?
end

def redirect_to_builder_embed_if_v3
# @visualization is not loaded if the embed is cached
# Changing version invalidates the embed cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class VisualizationPublicMapAdapter
:overlays, :created_at, :updated_at, :description, :mapviews, :geometry_types, :privacy, :tags,
:surrogate_key, :has_password?, :total_mapviews, :is_viewable_by_user?, :is_accesible_by_user?,
:can_be_cached?, :is_privacy_private?, :source, :kind_raster?, :has_read_permission?, :has_write_permission?,
:open_in_editor?, :version, :kind, :public?, :public_with_link?, :user_table, :liked_by?, :kuviz?
:open_in_editor?, :version, :kind, :public?, :public_with_link?, :user_table, :liked_by?, :kuviz?, :app?
] => :visualization

attr_reader :visualization
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/carto/api/public/app_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Carto
module Api
module Public
class AppPresenter

def initialize(context, user, app)
@context = context
@user = user
@app = app
end

def to_hash
{
id: @app.id,
name: @app.name,
privacy: @app.privacy,
created_at: @app.created_at,
updated_at: @app.updated_at,
url: CartoDB.url(@context, 'app_show',
params: { id: @app.id },
user: @user)
}
end
end
end
end
end
167 changes: 167 additions & 0 deletions app/controllers/carto/api/public/apps_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
require_relative '../visualization_searcher'
require_relative '../paged_searcher'

class Carto::Api::Public::AppsController < Carto::Api::Public::ApplicationController
include Carto::ControllerHelper
include Carto::Api::VisualizationSearcher
include Carto::Api::PagedSearcher

CONTENT_LENGTH_LIMIT_IN_BYTES = 10 * 1024 * 1024 # 10MB
VALID_ORDER_PARAMS = %i(name updated_at privacy).freeze
ALLOWED_PRIVACY_MODES = [
Carto::Visualization::PRIVACY_PUBLIC,
Carto::Visualization::PRIVACY_PROTECTED
].freeze

ssl_required

before_action :check_master_api_key
before_action :validate_mandatory_creation_params, only: [:create]
before_action :validate_input_parameters, only: [:create, :update]
before_action :get_app, only: [:update, :delete]
before_action :get_user, only: [:create, :update, :delete]
before_action :check_edition_permission, only: [:update, :delete]

rescue_from Carto::UnauthorizedError, with: :rescue_from_carto_error

def index
opts = { valid_order_combinations: VALID_ORDER_PARAMS }
page, per_page, order, order_direction = page_per_page_order_params(VALID_ORDER_PARAMS, opts)
params[:type] = Carto::Visualization::TYPE_APP
vqb = query_builder_with_filter_from_hash(params)

apps = vqb.with_order(order, order_direction).build_paged(page, per_page).map do |v|
Carto::Api::Public::AppPresenter.new(self, v.user, v).to_hash
end
response = {
apps: apps,
total_entries: vqb.build.size
}
render_jsonp(response)
rescue Carto::ParamInvalidError => e
CartoDB::Logger.error(exception: e)
render_jsonp({ error: e.message }, 400)
rescue StandardError => e
CartoDB::Logger.error(exception: e)
render_jsonp({ error: e.message }, 500)
end

def create
app = create_visualization_metadata(@logged_user)
asset = Carto::Asset.for_visualization(visualization: app,
resource: StringIO.new(Base64.decode64(params[:data])))
asset.save
# Carto::Tracking::Events::CreatedMap.new(@logged_user.id, event_properties(app).merge(origin: 'custom')).report

render_jsonp(Carto::Api::Public::AppPresenter.new(self, @logged_user, app).to_hash, 200)
rescue ActiveRecord::RecordInvalid => e
CartoDB::Logger.error(message: 'Error creating app', params: params, exception: e)
render_jsonp({ error: e.message }, 400)
rescue StandardError => e
CartoDB::Logger.error(message: 'Error creating app', params: params, exception: e)
render_jsonp({ error: 'The app can not be created' }, 500)
end

def update
@app.update_attributes!(params.permit(:name, :privacy, :password))

if params[:data].present?
@app.asset.update_visualization_resource(StringIO.new(Base64.decode64(params[:data])))
# In case we only update the asset we need to invalidate the visualization
@app.save
end
# Carto::Tracking::Events::ModifiedMap.new(@logged_user.id, event_properties(@app)).report

render_jsonp(Carto::Api::Public::AppPresenter.new(self, @logged_user, @app).to_hash, 200)
rescue ActiveRecord::RecordInvalid => e
render_jsonp({ error: e.message }, 400)
end

def delete
# Carto::Tracking::Events::DeletedMap.new(@logged_user.id, event_properties(@app)).report
@app.destroy
head 204
rescue StandardError => e
CartoDB::Logger.error(message: 'Error deleting app', exception: e, visualization: @app)
render_jsonp({ errors: [e.message] }, 400)
end

private

def create_visualization_metadata(user)
app = Carto::Visualization.new
app.name = params[:name]
app.privacy = params[:password].present? ? Carto::Visualization::PRIVACY_PROTECTED : Carto::Visualization::PRIVACY_PUBLIC
app.password = params[:password]
app.type = Carto::Visualization::TYPE_APP
app.user = user
app.save!
app
end

# def event_properties(app)
# {
# user_id: @logged_user.id,
# app_id: app.id
# }
# end

def get_user
@logged_user = current_viewer.present? ? Carto::User.find(current_viewer.id) : nil
end

def check_master_api_key
api_key = Carto::ApiKey.find_by_token(params["api_key"])
raise Carto::UnauthorizedError unless api_key&.master?
end

def check_edition_permission
head(403) unless @app.has_permission?(@logged_user, Carto::Permission::ACCESS_READWRITE)
end

def validate_input_parameters
if request.content_length > CONTENT_LENGTH_LIMIT_IN_BYTES
return render_jsonp({ error: "Visualization over the size limit (#{CONTENT_LENGTH_LIMIT_IN_BYTES / 1024 / 1024}MB)" }, 400)
end

if params[:privacy].present?
unless ALLOWED_PRIVACY_MODES.include?(params[:privacy])
return render_jsonp({ error: "privacy mode not allowed. Allowed ones are #{ALLOWED_PRIVACY_MODES}" }, 400)
end
if params[:privacy] == Carto::Visualization::PRIVACY_PROTECTED && !params[:password].present?
return render_jsonp({ error: 'Changing privacy to protected should come along with the password param' }, 400)
end
end

if params[:data].present?
begin
decoded_data = Base64.strict_decode64(params[:data])
return render_jsonp({ error: 'data parameter must be HTML' }, 400) unless html_param?(decoded_data)
rescue ArgumentError
return render_jsonp({ error: 'data parameter must be encoded in base64' }, 400)
end
end
end

def validate_mandatory_creation_params
if !params[:data].present?
render_jsonp({ error: 'missing data parameter' }, 400)
elsif !params[:name].present?
render_jsonp({ error: 'missing name parameter' }, 400)
end
end

def html_param?(data)
# FIXME this is a very naive implementantion. I'm trying to use
# Nokogiri to validate the HTML but it doesn't works as I want
# so
data.match(/\<html.*\>/).present?
end

def get_app
@app = Carto::Visualization.find(params[:id])
if @app.nil?
raise Carto::LoadError.new('App doesn\'t exist', 404)
end
end
end
12 changes: 12 additions & 0 deletions app/controllers/carto/api/visualization_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def privacy_aware_map_url(additional_params = {}, action = 'public_visualization
return unless organization

return kuviz_url(@visualization) if @visualization.kuviz?
return app_url(@visualization) if @visualization.app?

# When a visualization is private, checks of permissions need not only the Id but also the vis owner database schema
# Logic on public_map route will handle permissions so here we only "namespace the id" when proceeds
Expand Down Expand Up @@ -195,6 +196,13 @@ def kuviz_url(visualization)
"#{CartoDB.base_url(org_name, username)}#{path}"
end

def app_url(visualization)
org_name = visualization.user.organization.name
username = visualization.user.username
path = CartoDB.path(@context, 'app_show', id: visualization.id)
"#{CartoDB.base_url(org_name, username)}#{path}"
end

private

attr_reader :related, :load_related_canonical_visualizations, :show_user,
Expand Down Expand Up @@ -271,6 +279,10 @@ def url
CartoDB.url(@context, 'kuviz_show',
params: { id: @visualization.id },
user: @current_viewer)
elsif @visualization.app?
CartoDB.url(@context, 'app_show',
params: { id: @visualization.id },
user: @current_viewer)
else
CartoDB.url(@context, 'public_visualizations_show_map',
params: { id: @visualization.id },
Expand Down
71 changes: 71 additions & 0 deletions app/controllers/carto/app/visualizations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require_dependency 'carto/helpers/frame_options_helper'

module Carto
module App
class VisualizationsController < ApplicationController
include Carto::FrameOptionsHelper

ssl_required

before_action :x_frame_options_allow, only: [:show, :show_protected]
before_action :get_app

skip_before_filter :verify_authenticity_token, only: [:show_protected]

def show
return app_password_protected if show_password?

@source = HTMLAssetsService.instance.read_source_data(@app.asset)
add_cache_headers
render layout: false
rescue StandardError => e
CartoDB::Logger.error(exception: e)
render_404
end

def show_protected
submitted_password = params.fetch(:password, nil)
return(render_404) unless @app.password_protected? && @app.has_password?

unless @app.password_valid?(submitted_password)
flash[:placeholder] = '*' * (submitted_password ? submitted_password.size : DEFAULT_PLACEHOLDER_CHARS)
flash[:error] = "Invalid password"
return app_password_protected
end

@source = HTMLAssetsService.instance.read_source_data(@app.asset)
add_cache_headers

render 'show', layout: false
rescue StandardError => e
CartoDB::Logger.error(exception: e)
app_password_protected
end

private

def show_password?
return false if current_user && @app.has_read_permission?(current_user)

@app.password_protected?
end

def get_app
@app = Carto::Visualization.find(params[:id])
if @app.nil?
raise Carto::LoadError.new('App doesn\'t exist', 404)
end
end

def app_password_protected
render 'app_password', layout: 'application_password_layout'
end

def add_cache_headers
response.headers['X-Cache-Channel'] = "#{@app.varnish_key}:vizjson"
response.headers['Surrogate-Key'] = "#{CartoDB::SURROGATE_NAMESPACE_PUBLIC_PAGES} #{@app.surrogate_key}"
response.headers['Cache-Control'] = "no-cache,max-age=86400,must-revalidate,public"
end
end
end
end
4 changes: 2 additions & 2 deletions app/controllers/carto/kuviz/visualizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class VisualizationsController < ApplicationController
def show
return kuviz_password_protected if show_password?

@source = KuvizAssetsService.instance.read_source_data(@kuviz.asset)
@source = HTMLAssetsService.instance.read_source_data(@kuviz.asset)
add_cache_headers
render layout: false
rescue StandardError => e
Expand All @@ -33,7 +33,7 @@ def show_protected
return kuviz_password_protected
end

@source = KuvizAssetsService.instance.read_source_data(@kuviz.asset)
@source = HTMLAssetsService.instance.read_source_data(@kuviz.asset)
add_cache_headers

render 'show', layout: false
Expand Down
Loading

0 comments on commit 40eeb61

Please sign in to comment.