From 22789e0201851a5eddfbfb339affecc74fd84cee Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 3 Jun 2020 14:45:23 -0400
Subject: [PATCH] New `bootstrap.json` endpoint for starting up Discourse

Discourse needs a bunch of data preloaded before it can start up.
Normally we throw blobs of this into the HTML document that is requested
but in some cases that's awkward to retrieve.

For example with Ember CLI you have a separate javascript application
that needs to make its own HTML.

This API endpoint returns a JSON object with all the data Discourse needs to
bootstrap and start up.
---
 app/controllers/bootstrap_controller.rb    | 29 ++++++++++++++++
 app/helpers/application_helper.rb          |  1 +
 config/routes.rb                           |  2 ++
 spec/requests/bootstrap_controller_spec.rb | 40 ++++++++++++++++++++++
 4 files changed, 72 insertions(+)
 create mode 100644 app/controllers/bootstrap_controller.rb
 create mode 100644 spec/requests/bootstrap_controller_spec.rb

diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb
new file mode 100644
index 00000000000..dca7007c586
--- /dev/null
+++ b/app/controllers/bootstrap_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class BootstrapController < ApplicationController
+  include ApplicationHelper
+  include ActionView::Helpers::AssetUrlHelper
+
+  # This endpoint allows us to produce the data required to start up Discourse via JSON API,
+  # so that you don't have to scrape the HTML for `data-*` payloads
+  def index
+    locale = script_asset_path("locales/#{I18n.locale}")
+
+    preload_anonymous_data
+    if current_user
+      current_user.sync_notification_channel_position
+      preload_current_user_data
+    end
+
+    bootstrap = {
+      theme_ids: theme_ids,
+      title: SiteSetting.title,
+      current_homepage: current_homepage,
+      locale_script: "#{Discourse.base_url}#{locale}",
+      setup_data: client_side_setup_data,
+      preloaded: @preloaded
+    }
+
+    render_json_dump(bootstrap: bootstrap)
+  end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 482b2477d3d..f6bda658a38 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -356,6 +356,7 @@ module ApplicationHelper
   end
 
   def loading_admin?
+    return false unless defined?(controller)
     controller.class.name.split("::").first == "Admin"
   end
 
diff --git a/config/routes.rb b/config/routes.rb
index 69ade71938a..e763252bc69 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -14,6 +14,8 @@ Discourse::Application.routes.draw do
   match "/404", to: "exceptions#not_found", via: [:get, :post]
   get "/404-body" => "exceptions#not_found_body"
 
+  get "/bootstrap" => "bootstrap#index"
+
   post "webhooks/aws" => "webhooks#aws"
   post "webhooks/mailgun"  => "webhooks#mailgun"
   post "webhooks/mailjet"  => "webhooks#mailjet"
diff --git a/spec/requests/bootstrap_controller_spec.rb b/spec/requests/bootstrap_controller_spec.rb
new file mode 100644
index 00000000000..6495eb4d282
--- /dev/null
+++ b/spec/requests/bootstrap_controller_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe BootstrapController do
+
+  it "returns data as anonymous" do
+    get "/bootstrap.json"
+    expect(response.status).to eq(200)
+
+    json = response.parsed_body
+    expect(json).to be_present
+
+    bootstrap = json['bootstrap']
+    expect(bootstrap).to be_present
+    expect(bootstrap['title']).to be_present
+    expect(bootstrap['setup_data']['base_url']).to eq(Discourse.base_url)
+    preloaded = bootstrap['preloaded']
+    expect(preloaded['site']).to be_present
+    expect(preloaded['siteSettings']).to be_present
+    expect(preloaded['currentUser']).to be_blank
+    expect(preloaded['topicTrackingStates']).to be_blank
+  end
+
+  it "returns user data when authenticated" do
+    user = Fabricate(:user)
+    sign_in(user)
+    get "/bootstrap.json"
+    expect(response.status).to eq(200)
+
+    json = response.parsed_body
+    expect(json).to be_present
+
+    bootstrap = json['bootstrap']
+    preloaded = bootstrap['preloaded']
+    expect(preloaded['currentUser']).to be_present
+    expect(preloaded['topicTrackingStates']).to be_present
+  end
+
+end