diff --git a/app/assets/images/logos/book-small.png b/app/assets/images/logos/book-small.png index d19bbfcf..96733bdd 100644 Binary files a/app/assets/images/logos/book-small.png and b/app/assets/images/logos/book-small.png differ diff --git a/app/assets/images/screenshots/integration-references.png b/app/assets/images/screenshots/integration-references.png new file mode 100644 index 00000000..c3b5b640 Binary files /dev/null and b/app/assets/images/screenshots/integration-references.png differ diff --git a/app/assets/images/screenshots/integrations.png b/app/assets/images/screenshots/integrations.png new file mode 100644 index 00000000..2425a3c1 Binary files /dev/null and b/app/assets/images/screenshots/integrations.png differ diff --git a/app/assets/images/screenshots/page-types.png b/app/assets/images/screenshots/page-types.png new file mode 100644 index 00000000..d5685922 Binary files /dev/null and b/app/assets/images/screenshots/page-types.png differ diff --git a/app/assets/javascripts/api_docs.coffee b/app/assets/javascripts/api_docs.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/api_docs.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/integration_authorizations.coffee b/app/assets/javascripts/integration_authorizations.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/integration_authorizations.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/api_docs.scss b/app/assets/stylesheets/api_docs.scss new file mode 100644 index 00000000..5d0b541a --- /dev/null +++ b/app/assets/stylesheets/api_docs.scss @@ -0,0 +1,21 @@ +.api-docs { + h1 { + font-size: 24px; + } + + h2 { + font-size: 20px; + } + + h3 { + font-size: 16px; + } + + .code { + font-family: monospace; + color: white; + background: black; + white-space: pre-wrap; + padding: 0 20px; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/integration_authorizations.scss b/app/assets/stylesheets/integration_authorizations.scss new file mode 100644 index 00000000..43397831 --- /dev/null +++ b/app/assets/stylesheets/integration_authorizations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the IntegrationAuthorizations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/sidenav.css.scss b/app/assets/stylesheets/sidenav.css.scss index cff23eaf..acb8221f 100644 --- a/app/assets/stylesheets/sidenav.css.scss +++ b/app/assets/stylesheets/sidenav.css.scss @@ -41,3 +41,7 @@ transform: rotate(-90deg); } } + +#page-lookup-list { + min-width: 33em; +} \ No newline at end of file diff --git a/app/authorizers/content_authorizer.rb b/app/authorizers/content_authorizer.rb index c8d7f28d..ef17875d 100644 --- a/app/authorizers/content_authorizer.rb +++ b/app/authorizers/content_authorizer.rb @@ -1,11 +1,11 @@ class ContentAuthorizer < ApplicationAuthorizer def readable_by? user [ - PermissionService.user_owns_any_containing_universe?(user: user, content: resource), - PermissionService.user_owns_content?(user: user, content: resource), - PermissionService.content_is_public?(content: resource), - PermissionService.content_is_in_a_public_universe?(content: resource), - PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource) + ::PermissionService.user_owns_any_containing_universe?(user: user, content: resource), + ::PermissionService.user_owns_content?(user: user, content: resource), + ::PermissionService.content_is_public?(content: resource), + ::PermissionService.content_is_in_a_public_universe?(content: resource), + ::PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource) ].any? end diff --git a/app/controllers/api/api_docs_controller.rb b/app/controllers/api/api_docs_controller.rb new file mode 100644 index 00000000..66c7b78c --- /dev/null +++ b/app/controllers/api/api_docs_controller.rb @@ -0,0 +1,29 @@ +module Api + class ApiDocsController < ApplicationController + layout 'developer', except: [:integrations] + + before_action :authenticate_user!, except: [:index, :docs, :references] + + def index + end + + def docs + end + + def integrations + end + + def pricing + end + + def applications + @applications = current_user.application_integrations + end + + def approvals + end + + def references + end + end +end \ No newline at end of file diff --git a/app/controllers/api/application_integrations_controller.rb b/app/controllers/api/application_integrations_controller.rb new file mode 100644 index 00000000..9025fde2 --- /dev/null +++ b/app/controllers/api/application_integrations_controller.rb @@ -0,0 +1,69 @@ +module Api + class ApplicationIntegrationsController < ApplicationController + layout 'developer' + + before_action :authenticate_user! + before_action :set_integration, only: [:show, :authorize, :edit, :update, :destroy] + + # GET /application_integrations + def index + @applications = current_user.application_integrations + end + + # GET /application_integrations/1 + def show + end + + def authorize + end + + # GET /application_integrations/new + def new + @integration = ApplicationIntegration.new + end + + # GET /application_integrations/1/edit + def edit + end + + # POST /application_integrations + def create + @integration = ApplicationIntegration.new(application_integration_params.merge({user: current_user})) + + if @integration.save + redirect_to api_application_path(@integration), notice: 'Application integration was successfully created.' + else + render :new + end + end + + # PATCH/PUT /application_integrations/1 + def update + if @integration.update(application_integration_params) + redirect_to @integration, notice: 'Application integration was successfully updated.' + else + render :edit + end + end + + # DELETE /application_integrations/1 + def destroy + @integration.destroy + redirect_to application_integrations_url, notice: 'Application integration was successfully destroyed.' + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_integration + @application_integration = current_user.application_integrations.find_by(id: params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def application_integration_params + params.require(:application_integration).permit( + :name, :description, :organization_name, :organization_url, :website_url, :privacy_policy_url, :authorization_callback_url + ) + end + end +end \ No newline at end of file diff --git a/app/controllers/api/integration_authorizations_controller.rb b/app/controllers/api/integration_authorizations_controller.rb new file mode 100644 index 00000000..e5a47643 --- /dev/null +++ b/app/controllers/api/integration_authorizations_controller.rb @@ -0,0 +1,24 @@ +module Api + class IntegrationAuthorizationsController < ApplicationController + protect_from_forgery + + def create + authorization = IntegrationAuthorization.create(integration_authorization_params.merge({ + user_id: current_user.id, + referral_url: request.referrer, + ip_address: request.remote_ip, + origin: request.headers['HTTP_ORIGIN'], + content_type: request.headers['CONTENT_TYPE'], + user_agent: request.headers['HTTP_USER_AGENT'], + user_token: SecureRandom.hex(24) + })) + return redirect_to(authorization.application_integration.authorization_callback_url + "?token=#{authorization.user_token}") + end + + private + + def integration_authorization_params + params.require(:integration_authorization).permit(:application_integration_id) + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 92bf5cb2..74a6d6cc 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -1,6 +1,96 @@ module Api module V1 class ApiController < ApplicationController + before_action :initialize_updates_used_tracker + + before_action :authenticate_application! + before_action :authenticate_api_user! + + after_action :log_api_request + + def authenticate_application! + @application_integration = ApplicationIntegration.find_by(application_token: params[:application_token]) + + unless @application_integration + @request_success = :error + log_api_request + + render json: { + "Error" => "Invalid application_token", + "Param" => "application_token", + "Token" => params[:application_token] + } + return + end + end + + def authenticate_api_user! + @authorization = @application_integration.integration_authorizations.find_by(user_token: params[:authorization_token]) + # todo error on this if not set + + @current_api_user = @authorization.try(:user) + unless @current_api_user + @request_success = :error + log_api_request + + render json: { + "Error" => "Invalid authorization_token", + "Param" => "authorization_token", + "Token" => params[:authorization_token] + } + return + end + end + + def initialize_updates_used_tracker + @updates_used_this_request = 0 + end + + def log_api_request + ApiRequest.create!( + application_integration: @application_integration, + integration_authorization: @authorization, + result: @request_success || :success, + updates_used: @updates_used_this_request, + ip_address: request.remote_ip + ) + end + + # Content page list endpoints + Rails.application.config.content_types[:all].each do |content_type| + define_method(content_type.name.downcase.pluralize) do + pages = @current_api_user.send(content_type.name.downcase.pluralize) + + render json: pages.map { |page| + { + id: page.id, + name: page.name, + description: page.description, + universe: page.try(:universe).nil? ? nil : { + id: page.universe_id, + name: page.universe.name + }, + meta: { + created_at: page.created_at, + updated_at: page.updated_at + } + } + } + end + end + + # Content page show endpoints + Rails.application.config.content_types[:all].each do |content_type| + define_method(content_type.name.downcase) do + page = content_type.find_by(id: params[:id]) + + if page && page.readable_by?(@current_api_user || User.new) + render json: ApiContentSerializer.new(page, include_blank_fields: params.fetch(:include_blank_fields, false)).data + else + render json: { error: "Page not found" } + end + end + end end end end \ No newline at end of file diff --git a/app/helpers/api_docs_helper.rb b/app/helpers/api_docs_helper.rb new file mode 100644 index 00000000..a5054aa8 --- /dev/null +++ b/app/helpers/api_docs_helper.rb @@ -0,0 +1,2 @@ +module ApiDocsHelper +end diff --git a/app/helpers/application_integrations_helper.rb b/app/helpers/application_integrations_helper.rb new file mode 100644 index 00000000..861ebf7c --- /dev/null +++ b/app/helpers/application_integrations_helper.rb @@ -0,0 +1,2 @@ +module ApplicationIntegrationsHelper +end diff --git a/app/helpers/integration_authorizations_helper.rb b/app/helpers/integration_authorizations_helper.rb new file mode 100644 index 00000000..7c5fc8e2 --- /dev/null +++ b/app/helpers/integration_authorizations_helper.rb @@ -0,0 +1,2 @@ +module IntegrationAuthorizationsHelper +end diff --git a/app/javascript/components/PageLookupSidebar.js b/app/javascript/components/PageLookupSidebar.js index 209705a1..6fcc8364 100644 --- a/app/javascript/components/PageLookupSidebar.js +++ b/app/javascript/components/PageLookupSidebar.js @@ -13,16 +13,17 @@ import Divider from '@material-ui/core/Divider'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; -import InboxIcon from '@material-ui/icons/MoveToInbox'; -import MailIcon from '@material-ui/icons/Mail'; import Button from '@material-ui/core/Button'; import ListSubheader from '@material-ui/core/ListSubheader'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import StarBorder from '@material-ui/icons/StarBorder'; +import HelpIcon from '@material-ui/icons/Help'; import Collapse from '@material-ui/core/Collapse'; +import axios from 'axios'; + class PageLookupSidebar extends React.Component { constructor(props) { @@ -36,56 +37,37 @@ class PageLookupSidebar extends React.Component { }; } - loadPage(page_type, page_id) { + async loadPage(page_type, page_id) { this.setDrawerVisible(true); - - // show loading icon + this.setState({ show_data: false }); // make api request - - // hide loading icon - - // load response into list - this.setState({ - page_data: { - name: page_type + ' ' + 'Bob', - categories: [ - { - id: 1, - label: 'General', - fields: [ - { - label: 'Name', - value: 'Bob', - type: 'text' - }, - { - label: 'Age', - value: '55', - type: 'text' - } - ] - }, - { - id: 2, - label: 'Family', - fields: [ - { - label: 'Mom', - value: 'Robin', - type: 'link', - link: ['Character', 534] - } - - ] - } - ] + await axios.get( + "/api/v1/" + page_type.toLowerCase() + "/" + page_id + + '?application_token=4756de490e82956dc6329e6650aaec664e27ccd27e153e2f' + + '&authorization_token=167bb93139303904cf67f6480a29e71c9f1eaf7a28e902e1', + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } } + ).then(response => { + console.log("get request"); + console.log(response.data); + + // load response into list + this.setState({ + page_data: response.data, + show_data: true, + page_type: page_type + }); + + }).catch(err => { + console.log(err); + return null; }); - console.log("setting show_data = true"); - this.setState({ show_data: true }); - // this.state.show_data = true; console.log("show data? " + this.state.show_data); }; @@ -99,6 +81,37 @@ class PageLookupSidebar extends React.Component { }}); } + fieldData(field) { + switch (field.type) { + case "name": + case "text_area": + return ( + + + + ); + + case "link": + return( + + + {field.label} + + {field.value.map((link) => ( + + + + ))} + + ); + + default: + return( +
error loading {field.label}
+ ); + } + } + pageData() { if (this.state.show_data) { return ( @@ -106,22 +119,21 @@ class PageLookupSidebar extends React.Component { role="presentation" > - Quick-reference - +
   {this.state.page_data.name}
} + id="page-lookup-list" > - - - + + + Quick-reference + {this.state.page_data.categories.map((category) => ( - + this.toggleCategoryOpen(category.label)}> - + {!!this.state.category_open[category.label] ? : } @@ -129,22 +141,29 @@ class PageLookupSidebar extends React.Component { {category.fields.map((field) => ( - - {field.type == 'text' && - - } - {field.type == 'link' && - - } - + this.fieldData(field) ))} ))} + + + + + + + + + + + + +
); + // also add divider + view/edit links } else { // No data to show yet return ( @@ -152,9 +171,8 @@ class PageLookupSidebar extends React.Component { role="presentation" > + Quick-reference } @@ -163,6 +181,49 @@ class PageLookupSidebar extends React.Component { +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
); } @@ -176,7 +237,8 @@ class PageLookupSidebar extends React.Component { this.setDrawerVisible(false)}> + onClose={() => this.setDrawerVisible(false)} + > {this.pageData()} @@ -184,7 +246,7 @@ class PageLookupSidebar extends React.Component { } } -// PrivacyToggle.propTypes = { +// PageLookupSidebar.propTypes = { // content: PropTypes.exact({ // id: PropTypes.number.isRequired, // name: PropTypes.string.isRequired, diff --git a/app/models/api_request.rb b/app/models/api_request.rb new file mode 100644 index 00000000..06af48bc --- /dev/null +++ b/app/models/api_request.rb @@ -0,0 +1,7 @@ +class ApiRequest < ApplicationRecord + belongs_to :application_integration, optional: true + belongs_to :integration_authorization, optional: true + + scope :successful, -> { where(result: 'success') } + scope :errored, -> { where(result: 'error') } +end diff --git a/app/models/application_integration.rb b/app/models/application_integration.rb new file mode 100644 index 00000000..c7797f25 --- /dev/null +++ b/app/models/application_integration.rb @@ -0,0 +1,47 @@ +class ApplicationIntegration < ApplicationRecord + belongs_to :user + + has_many :integration_authorizations + has_many :api_requests + + after_create :generate_new_access_token! + + def self.icon + 'extension' + end + + def self.color + 'orange' + end + + def generate_new_access_token! + self.update(application_token: SecureRandom.hex(24)) + end + + def request_error_rate + @request_error_rate ||= begin + errored_requests = api_requests.errored.count + total_requests = api_requests.count + + return 0 if total_requests.zero? + (errored_requests.to_f / total_requests).round(3) + end + end + + def error_rate_color + rate = request_error_rate + + case rate + when 0.0..0.1 + 'green' + when 0.1..0.3 + 'yellow' + when 0.3..1 + 'red' + end + end + + def current_quota_usage_percentage + @current_quota_usage_percentage ||= ((api_requests.successful.count.to_f / 10_000) * 100).round(2) + end +end diff --git a/app/models/concerns/has_attributes.rb b/app/models/concerns/has_attributes.rb index d1a863d0..90f4a754 100644 --- a/app/models/concerns/has_attributes.rb +++ b/app/models/concerns/has_attributes.rb @@ -8,25 +8,36 @@ module HasAttributes after_save :update_custom_attributes def self.create_default_attribute_categories(user) + # Don't create any attribute categories for AttributeCategories or AttributeFields that share the ContentController return [] if ['attribute_category', 'attribute_field'].include?(content_name) - YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details| - category = user.attribute_categories.create!( + YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, defaults| + # First, query for the category to see if it already exists + category = user.attribute_categories.find_or_initialize_by( entity_type: self.content_name, - name: category_name.to_s, - icon: details[:icon], - label: details[:label] + name: category_name.to_s ) + creating_new_category = category.new_record? - category.attribute_fields << details[:attributes].map do |field| - af_field = category.attribute_fields.with_deleted.create!( - old_column_source: field[:name], - user: user, - field_type: field[:field_type].presence || "text_area", - label: field[:label].presence || 'Untitled field' - ) - af_field - end if details.key?(:attributes) + # If the category didn't already exist, go ahead and set defaults on it and save + if creating_new_category + category.label = defaults[:label] + category.icon = defaults[:icon] + category.save! + end + + # If we created this category for the first time, we also want to make sure we create its default fields, too + if creating_new_category && defaults.key?(:attributes) + category.attribute_fields << defaults[:attributes].map do |field| + af_field = category.attribute_fields.with_deleted.create!( + old_column_source: field[:name], + user: user, + field_type: field[:field_type].presence || "text_area", + label: field[:label].presence || 'Untitled field' + ) + af_field + end + end end.compact end @@ -37,7 +48,7 @@ module HasAttributes @cached_attribute_categories_for_this_content = begin # Always include the flatfile categories (but create AR versions if they don't exist) categories = YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details| - category = AttributeCategory.with_deleted.find_or_initialize_by( + category = ::AttributeCategory.with_deleted.find_or_initialize_by( entity_type: self.content_name, name: category_name.to_s, user: user @@ -102,19 +113,19 @@ module HasAttributes # # Always include the flatfile categories (but create AR versions if they don't exist) # categories = YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details| - # category = AttributeCategory.with_deleted.find_by( + # category = ::AttributeCategory.with_deleted.find_by( # entity_type: self.content_name, # name: category_name.to_s, # user: user # ) - # category.attribute_fields << details[:attributes].map do |field| - # category.attribute_fields.with_deleted.find_by( - # user: user, - # old_column_source: field[:name], - # field_type: field[:field_type].presence || "text_area" - # ) - # end if details.key?(:attributes) + # # category.attribute_fields << details[:attributes].map do |field| + # # category.attribute_fields.with_deleted.find_by( + # # user: user, + # # old_column_source: field[:name], + # # field_type: field[:field_type].presence || "text_area" + # # ) + # # end if details.key?(:attributes) # if show_hidden # category diff --git a/app/models/concerns/is_content_page.rb b/app/models/concerns/is_content_page.rb index 6d15307e..8530299f 100644 --- a/app/models/concerns/is_content_page.rb +++ b/app/models/concerns/is_content_page.rb @@ -38,7 +38,7 @@ module IsContentPage else # For all other content pages, we have to fetch document IDs off DocumentEntities that # match those content pages - document_ids = DocumentAnalysis.where( + document_ids = ::DocumentAnalysis.where( id: document_entities.pluck(:document_analysis_id) ).pluck(:document_id) Document.where(id: document_ids) diff --git a/app/models/integration_authorization.rb b/app/models/integration_authorization.rb new file mode 100644 index 00000000..8d05e473 --- /dev/null +++ b/app/models/integration_authorization.rb @@ -0,0 +1,6 @@ +class IntegrationAuthorization < ApplicationRecord + belongs_to :user + belongs_to :application_integration + + has_many :api_requests +end diff --git a/app/models/page_types/country.rb b/app/models/page_types/country.rb index 7bcf3f42..ea40ef2d 100644 --- a/app/models/page_types/country.rb +++ b/app/models/page_types/country.rb @@ -8,7 +8,6 @@ class Country < ApplicationRecord include BelongsToUniverse - include HasAttributes include IsContentPage include Serendipitous::Concern diff --git a/app/models/page_types/language.rb b/app/models/page_types/language.rb index 52fc5ffa..7bff56ca 100644 --- a/app/models/page_types/language.rb +++ b/app/models/page_types/language.rb @@ -8,7 +8,6 @@ class Language < ApplicationRecord include BelongsToUniverse - include HasAttributes include IsContentPage include Serendipitous::Concern diff --git a/app/models/page_types/town.rb b/app/models/page_types/town.rb index 76668282..9651dd90 100644 --- a/app/models/page_types/town.rb +++ b/app/models/page_types/town.rb @@ -8,7 +8,6 @@ class Town < ApplicationRecord include BelongsToUniverse - include HasAttributes include IsContentPage include Serendipitous::Concern diff --git a/app/models/serializers/api_content_serializer.rb b/app/models/serializers/api_content_serializer.rb new file mode 100644 index 00000000..fbb84e7e --- /dev/null +++ b/app/models/serializers/api_content_serializer.rb @@ -0,0 +1,74 @@ +# This is an implementation of ContentSerializer that only exposes public columns in the +# standard API format and should be preferred when possible. + +class ApiContentSerializer + attr_accessor :id, :name, :user, :universe + + attr_accessor :categories + attr_accessor :fields + attr_accessor :attribute_values + attr_accessor :page_tags + attr_accessor :documents + + attr_accessor :raw_model + attr_accessor :class_name, :class_color, :class_icon + + attr_accessor :data + + def initialize(content, include_blank_fields: false) + self.categories = content.class.attribute_categories(content.user).where(hidden: false).eager_load(attribute_fields: :attribute_values) + self.fields = AttributeField.where(attribute_category_id: self.categories.map(&:id), hidden: false) + self.attribute_values = Attribute.where(attribute_field_id: self.fields.map(&:id), entity_type: content.page_type, entity_id: content.id).order('created_at desc') + self.universe = (content.class.name == Universe.name) ? nil : content.universe + + self.raw_model = content + + self.page_tags = content.page_tags.pluck(:tag) || [] + self.documents = content.documents || [] + + self.data = { + name: content.try(:name), + description: content.try(:description), + universe: self.universe.nil? ? nil : { + id: self.universe.id, + name: self.universe.try(:name) + }, + meta: { + created_at: content.created_at, + updated_at: content.updated_at + }, + categories: self.categories.map { |category| + { + id: category.id, + label: category.label, + fields: category.attribute_fields.order(:position).map { |field| + { + id: field.id, + label: field.label, + type: field.field_type, + value: value_for(field, content) + } + }.reject { |field| !include_blank_fields && field[:value].empty? } + } + }.reject { |category| !include_blank_fields && category[:fields].empty? }, + references: [] + } + end + + def value_for(attribute_field, content) + case attribute_field.field_type + when 'link' + self.raw_model.send(attribute_field.old_column_source) + + when 'tags' + self.page_tags + + else # text_area, name, universe, etc + self.attribute_values.detect { |value| + value.entity_type == content.page_type && + value.entity_id == content.id && + value.attribute_field_id == attribute_field.id + }.try(:value) || "" + end + end +end diff --git a/app/models/users/user.rb b/app/models/users/user.rb index 08b8da8d..06d87a05 100644 --- a/app/models/users/user.rb +++ b/app/models/users/user.rb @@ -107,6 +107,8 @@ class User < ApplicationRecord message: "can't be larger than 500KB" } + has_many :application_integrations + def my_universe_ids @cached_universe_ids ||= universes.pluck(:id) end diff --git a/app/views/api/api_docs/approvals.html.erb b/app/views/api/api_docs/approvals.html.erb new file mode 100644 index 00000000..d4eb1299 --- /dev/null +++ b/app/views/api/api_docs/approvals.html.erb @@ -0,0 +1 @@ +approvals \ No newline at end of file diff --git a/app/views/api/api_docs/docs.html.erb b/app/views/api/api_docs/docs.html.erb new file mode 100644 index 00000000..32b4dcfa --- /dev/null +++ b/app/views/api/api_docs/docs.html.erb @@ -0,0 +1,41 @@ +
+
+ <%= link_to api_path do %> + <%= image_tag 'logos/both-original.png', style: 'width: 100%' %> + <% end %> +
+ +
+
SUPPORTED NOTEBOOK ENDPOINTS
+ +
+
\ No newline at end of file diff --git a/app/views/api/api_docs/endpoints/content/_create_a_new_content.html.erb b/app/views/api/api_docs/endpoints/content/_create_a_new_content.html.erb new file mode 100644 index 00000000..43ee0c11 --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_create_a_new_content.html.erb @@ -0,0 +1,93 @@ +

+ <%= content_type.icon %> + Create a new <%= content_type.name.downcase %> +

+
+
+

Endpoint

+

+POST /api/v1/<%= content_type.name.downcase.pluralize %> +

+

Example call

+ + <% if Rails.application.config.content_types[:premium].include?(content_type) %> +
+ Note: Because this is a Premium page, either you (the application) + or your authenticated user must have an active Premium subscription to create + a new <%= content_type.name.downcase %> page. +
+ <% end %> +
+
+

Example response

+

+{ + "id": 12345, + "name": "Some <%= content_type.name %>", +<% unless content_type.name == Universe.name %> + "universe_id": 2, +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + "categories": { + "Overview": { + "fields": [ + { + "id": 123, + "label": "Description", + "value": "Some Description" + }, + { + "id": 124, + "label": "Another Field", + "value": "Some other value" + }, + ... + ], + }, + ... + }, + "references": [...] +} +

+
+
diff --git a/app/views/api/api_docs/endpoints/content/_delete_a_specific_content.html.erb b/app/views/api/api_docs/endpoints/content/_delete_a_specific_content.html.erb new file mode 100644 index 00000000..b876a98d --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_delete_a_specific_content.html.erb @@ -0,0 +1,10 @@ +

+ <%= content_type.icon %> + Delete a specific <%= content_type.name.downcase %> +

+

+ Not supported: + Deleting <%= content_type.name.downcase.pluralize %> is currently not supported over the API. + Please ask your user to delete their <%= content_type.name.downcase %> manually, or + <%= link_to 'contact us', 'https://github.com/indentlabs/notebook' %> if you need this permission for your application. +

\ No newline at end of file diff --git a/app/views/api/api_docs/endpoints/content/_fetch_a_specific_content.html.erb b/app/views/api/api_docs/endpoints/content/_fetch_a_specific_content.html.erb new file mode 100644 index 00000000..0c91ffff --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_fetch_a_specific_content.html.erb @@ -0,0 +1,106 @@ +

+ <%= content_type.icon %> + Fetch a specific <%= content_type.name.downcase %> +

+
+
+

Endpoint

+

+GET /api/v1/<%= content_type.name.downcase %>/<id> +

+

Example call

+ +
+
+

Example response

+

+{ + "id": 1, + "name": "Some <%= content_type.name %>", + "description": "A description of this page", +<% unless content_type.name == Universe.name %> + "universe": { + "id": 134, + "name": My Super Amazing Universe + }, +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + "categories": [ + { + "id": 123, + "label": "Overview", + "fields": [ + { + "id": 123, + "type": "text_area", + "label": "Coolness factor", + "value": "Off the charts" + }, + { + "id": 124, + "label": "Related Buildings", + "type": "link", + "value": [ + { + "id": 345, + "type": "Building", + "name": "The Office of Examples", + }, + ... + ] + }, + ... + ], + }, + ... + ], + "references": [TBD] +} +

+
+
diff --git a/app/views/api/api_docs/endpoints/content/_fetch_all_content.html.erb b/app/views/api/api_docs/endpoints/content/_fetch_all_content.html.erb new file mode 100644 index 00000000..0dd4ce5f --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_fetch_all_content.html.erb @@ -0,0 +1,83 @@ +

+ <%= content_type.icon %> + Fetch all <%= content_type.name.downcase.pluralize %> +

+
+
+

Endpoint

+

+GET /api/v1/<%= content_type.name.downcase.pluralize %> +

+

Request options

+ +
+
+

Example response

+

+[ + { + "id": 1, + "name": "Some <%= content_type.name %>", + "description": "This is a user-supplied description of the page", +<% unless content_type.name == Universe.name %> + "universe": { + "id": 2, + "name": "The Great Story World" + } +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + }, + { + "id": 2, + "name": "Some other <%= content_type.name %>", + "description": "This is an even better page", +<% unless content_type.name == Universe.name %> + "universe": { + "id": 2, + "name": "The Great Story World" + } +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + }, + ... +] +

+
+
\ No newline at end of file diff --git a/app/views/api/api_docs/endpoints/content/_modify_a_specific_content.html.erb b/app/views/api/api_docs/endpoints/content/_modify_a_specific_content.html.erb new file mode 100644 index 00000000..66a3bd97 --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_modify_a_specific_content.html.erb @@ -0,0 +1,96 @@ +

+ <%= content_type.icon %> + Modify a specific <%= content_type.name.downcase %> +

+
+
+

Endpoint

+

+POST /api/v1/<%= content_type.name.downcase.pluralize %>/<id> +

+

Example call

+ +
+
+

Example response

+

+{ + "id": 1, + "name": "Some <%= content_type.name %>", +<% unless content_type.name == Universe.name %> + "universe_id": 2, +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + "categories": { + "Overview": { + "fields": [ + { + "id": 123, + "label": "Description", + "value": "Some Description" + }, + { + "id": 124, + "label": "Another Field", + "value": "Some other value" + }, + ... + ], + }, + ... + }, + "references": [...] +} +

+
+
\ No newline at end of file diff --git a/app/views/api/api_docs/endpoints/content/_reference_a_specific_content.html.erb b/app/views/api/api_docs/endpoints/content/_reference_a_specific_content.html.erb new file mode 100644 index 00000000..b5e8fa0a --- /dev/null +++ b/app/views/api/api_docs/endpoints/content/_reference_a_specific_content.html.erb @@ -0,0 +1,131 @@ +

+ <%= content_type.icon %> + Add an online reference to a specific <%= content_type.name.downcase %> +

+<% unless current_page?(api_references_path) %> +

+ <%= link_to "Click here to read more about how online references work on Notebook.ai.", api_references_path %> +

+<% end %> +
+
+

Endpoint

+

+POST /api/v1/<%= content_type.name.downcase.pluralize %>/<id>/references +

+

Example call

+ +
+
+

Example response

+

+{ + "id": 1, + "name": "Some <%= content_type.name %>", +<% unless content_type.name == Universe.name %> + "universe_id": 2, +<% end %> + "meta": { + "created_at": "2020-02-01 08:24:20 UTC", + "updated_at": "2020-02-09 06:57:12 UTC" + }, + "categories": { + "Overview": { + "fields": [ + { + "id": 123, + "label": "Description", + "value": "Some Description" + }, + { + "id": 124, + "label": "Another Field", + "value": "Some other value" + }, + ... + ], + }, + ... + }, + "references": [ + { + "url": "https://www.example.com/something/wow", + "title": "Reference name", + "description": "This is something really cool on the Internet that this <%= content_type.name.downcase %> appears in!", + "reference_image": "https://www.example.com/<%= content_type.name.downcase.pluralize %>/12345.png" + } + ] +} +

+
+
\ No newline at end of file diff --git a/app/views/api/api_docs/endpoints/users/_active_page_types.html.erb b/app/views/api/api_docs/endpoints/users/_active_page_types.html.erb new file mode 100644 index 00000000..1896595c --- /dev/null +++ b/app/views/api/api_docs/endpoints/users/_active_page_types.html.erb @@ -0,0 +1,65 @@ +

+ <%= User.icon %> + Fetch a user's active page types +

+
+
+

Endpoint

+

+GET /api/v1/<%= User.name.downcase.pluralize %>/<id>/active_page_types +

+

Example call

+ +
+
+

Example response

+

+{ + "id": 1, + "name": "Alice Quinn", + "username": "@alice", + "active_page_types": [ + "Character", + "Location", + "Building", + "Landmark", + "Town", + ... + ] +} +

+
+
+ + diff --git a/app/views/api/api_docs/endpoints/users/_authorization_token.html.erb b/app/views/api/api_docs/endpoints/users/_authorization_token.html.erb new file mode 100644 index 00000000..a5668086 --- /dev/null +++ b/app/views/api/api_docs/endpoints/users/_authorization_token.html.erb @@ -0,0 +1,56 @@ +

+ <%= User.icon %> + Fetch a user's authorization token +

+
+
+

Endpoint

+

+GET /api/v1/<%= User.name.downcase.pluralize %>/<id>/authorization +

+

Example call

+ +
+

+ Each user's authorization token is already passed to your app through your + <%= link_to 'callback URL', '#' %> when that user first authorizes your app and should be stored (and associated with your user) at that time. + However, this endpoint is useful if you need to retrieve a particular user's authorization token later. +

+

+ This endpoint will only return an authorization token if the user you're requesting has already authorized your app. +

+
+
+
+

Example response

+

+{ + "id": 1, + "token": "a1ed29894c458d0093f74ededa59debc953712d9b412c224" +} +

+
+
+ + diff --git a/app/views/api/api_docs/endpoints/users/_profile.html.erb b/app/views/api/api_docs/endpoints/users/_profile.html.erb new file mode 100644 index 00000000..aeccdbb1 --- /dev/null +++ b/app/views/api/api_docs/endpoints/users/_profile.html.erb @@ -0,0 +1,76 @@ +

+ <%= User.icon %> + Fetch a user's profile information +

+
+
+

Endpoint

+

+GET /api/v1/<%= User.name.downcase.pluralize %>/<id> +

+

Example call

+ +
+
+

Example response

+

+{ + "id": 1, + "name": "Alice Quinn", + "username": "@alice", + "meta": { + "referral_code": "ABCDEFGHI-JKLMNOP-QRSTUV-WXYZ", + "premium_active": true + }, + "profile": { + "bio": "Just a small-town girl", + "interests": "cooking, going on walks, living life, magic", + "website": "http://www.example.com", + "inspirations": "old Lovecraft stories", + "other_names": "Allie", + "occupation": "I exist solely as an example in some API documentation", + "favorite": { + "author": "Oscar Wilde", + "genre": "Fantasy", + "book": "Harry Potter and the Sorcerer's Stone", + "quote": "This isn't even a real quote!", + "page_type": "Creature" + } + } +} +

+
+
+ + diff --git a/app/views/api/api_docs/index.html.erb b/app/views/api/api_docs/index.html.erb new file mode 100644 index 00000000..87c98fb9 --- /dev/null +++ b/app/views/api/api_docs/index.html.erb @@ -0,0 +1,161 @@ +
+
+ <%= link_to api_path do %> + <%= image_tag 'logos/both-original.png', style: 'width: 100%' %> + <% end %> +
+ +
+
+
+
+ Supercharge your writing applications with Notebook.ai's world-class worldbuilding resources +
+ +
+
+
+

+ <% Rails.application.config.content_types[:all].each do |content_type| %> + <%= content_type.icon %> + <% end %> +

+
+

+ We now offer an API for developers that would like to take advantage of the + features and worlds in Notebook.ai. +

+
+

+ To get started, you'll want to <%= link_to 'register an application', api_applications_path %> + and get your API key. In order to access any user's notebook, you'll first need + them to <%= link_to 'authenticate your application', api_approvals_path %> and give you an + access key specific to their notebook. +

+
+
+

+ In other words, it's just three easy steps to get started: +

+ +
    +
  • +
    Step 1
    +
    +
    +

    + To receive an access token for your application, you'll first need to register it for use. +

    +
    +
    + <%= link_to 'Register application', api_applications_path, class: 'blue white-text btn' %> + +
    +
    +
  • +
  • +
    Step 2
    +
    +
    +

    + In order to access or modify a user's notebook pages on their behalf, you'll first need that user to + approve your application. You can set up an approval flow that notifies your application whenever + a user authorizes its use, or you can direct them to <%= link_to 'their integrations page', api_integrations_path %> + and have them paste their authorization code directly into your app. +

    +
    +
    + <%= link_to 'Create approval flow', api_approvals_path, class: 'btn blue white-text' %> + +
    +
    +
  • +
  • +
    Step 3
    + +
  • +
+
+
+
+
+
+ + <%= link_to api_docs_path do %> +
+
+
Browse available API endpoints
+

Documentation for developers, by developers

+
+
+ <% end %> + +
+
+
+ Gain full access to + <% Rails.application.config.content_types[:all_non_universe].each do |type| %> + <%= type.name.downcase.pluralize %>, + <% end %> + and universes. +
+
+
+

+ After a user authenticates your application, you'll have full access to integrate their worldbuilding pages into your app. + You can show them their characters, let them edit one of their existing creatures, pin their towns and landmarks to your maps, + create new items, and more — all without leaving your app. +

+
+

+ <%= link_to 'See the available endpoints by clicking here.', api_docs_path %> +

+
+
+ <%= image_tag 'screenshots/page-types.png', width: '100%', class: 'materialboxed', data: { caption: "A few of the page types available on Notebook.ai" } %> +
+
+
+
+ +
+
+
+ Generate two-way mentions for your content and drive traffic back to your site +
+
+
+ <%= image_tag 'screenshots/integrations.png', width: '100%', class: 'materialboxed', data: { caption: "Each integration gets its own tab on Notebook.ai pages" } %> +
+
+

+ When a user links their notebook page in your app, we can automatically show a link back to your page from that notebook page, too. +

+
+

+ For example, if a user publishes a story about Alice and Bob on your site and links their Alice and Bob pages from Notebook.ai, we'll + show a link to that story on both Alice and Bob's Notebook.ai pages also. +

+
+

+ <%= link_to 'See the available endpoints by clicking here.', api_docs_path %> +

+
+
+
+
+ +
+

+ Want to build something our API doesn't support yet? + <%= link_to 'Get in touch!', 'https://github.com/indentlabs/notebook/issues' %> +

+
+
\ No newline at end of file diff --git a/app/views/api/api_docs/integrations.html.erb b/app/views/api/api_docs/integrations.html.erb new file mode 100644 index 00000000..3b59c9d9 --- /dev/null +++ b/app/views/api/api_docs/integrations.html.erb @@ -0,0 +1 @@ +integrations \ No newline at end of file diff --git a/app/views/api/api_docs/pricing.html.erb b/app/views/api/api_docs/pricing.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/views/api/api_docs/references.html.erb b/app/views/api/api_docs/references.html.erb new file mode 100644 index 00000000..d7f721ee --- /dev/null +++ b/app/views/api/api_docs/references.html.erb @@ -0,0 +1,57 @@ +
+
+
+
+
+
+
Referencing pages around the Internet on Notebook.ai
+

+ Application developers can now add external links directly to individual Notebook.ai pages via <%= link_to 'the API', api_path %>. +

+
+

+ Each application receives its own tab under the "References" sidebar when viewing any notebook page, which will list + all external references when clicked. Each reference must have a URL, and may optionally also have a title, description, + and image to accompany it. +

+
+

+ Please use this API only to link pages in which the content of a particular Notebook.ai page appears or is predominantly featured. + For a character, for example, consider adding references to stories in which they appear. For a location, consider adding references + to stories set in that location. +

+
+
+ <%= image_tag 'screenshots/integration-references.png', class: 'hoverable' %> +
+
+
+ <%= link_to 'Browse the full API documentation', api_path, class: 'blue btn white-text' %> +
+
+
+ <%= image_tag 'screenshots/integrations.png', class: 'hoverable' %> +
+
+
+
+
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/app/views/api/application_integrations/_form.html.erb b/app/views/api/application_integrations/_form.html.erb new file mode 100644 index 00000000..a846f83a --- /dev/null +++ b/app/views/api/application_integrations/_form.html.erb @@ -0,0 +1,16 @@ +<%= form_for api_applications_path do |f| %> +

+ If you'd like to create an application that is able to use the <%= link_to 'Notebook.ai API', api_path %>, + please fill out the following form to get started. +

+
+ + <% [:name, :description, :organization_name, :organization_url, :website_url, :privacy_policy_url, :authorization_callback_url, :event_ping_url].each do |field| %> +
+ <%= f.label field.to_s.humanize %> + <%= f.text_field field, placeholder: field.to_s.humanize %> +
+ <% end %> + <%= f.submit 'Register application', class: 'btn white-text blue' %> + +<% end %> \ No newline at end of file diff --git a/app/views/api/application_integrations/authorize.html.erb b/app/views/api/application_integrations/authorize.html.erb new file mode 100644 index 00000000..124aca85 --- /dev/null +++ b/app/views/api/application_integrations/authorize.html.erb @@ -0,0 +1,37 @@ +<% 1.times do %>
<% end %> + +
+
+ <%= image_tag 'logos/book-small.png', width: '100%' %> +
Notebook.ai
+ add +
+
+
+
+
+ <%= ApplicationIntegration.icon %> + <%= @application_integration.name %> +
+

+

description
+ <%= simple_format @application_integration.description %> +

+
+

+ Authorizing this application will give it full access to manage your Notebook.ai pages, including your private pages. Please only authorize this application + if you trust the developer and are here on purpose. +

+
+

+ You can manage your authorized Notebook.ai integrations at any time in your <%= link_to 'Data Vault', '#' %>. +

+
+ <%= form_for [:api, IntegrationAuthorization.new({ user: current_user, application_integration: @application_integration })] do |f| %> + <%= f.hidden_field :application_integration_id, value: @application_integration.id %> + <%= f.submit "Authorize #{@application_integration.name}", class: "#{ApplicationIntegration.color} btn white-text", style: 'width: 100%' %> + <% end %> +
+
+
+
\ No newline at end of file diff --git a/app/views/api/application_integrations/edit.html.erb b/app/views/api/application_integrations/edit.html.erb new file mode 100644 index 00000000..bb71dce8 --- /dev/null +++ b/app/views/api/application_integrations/edit.html.erb @@ -0,0 +1,6 @@ +

Editing Application Integration

+ +<%= render 'form', application_integration: @application_integration %> + +<%= link_to 'Show', @application_integration %> | +<%= link_to 'Back', application_integrations_path %> diff --git a/app/views/api/application_integrations/index.html.erb b/app/views/api/application_integrations/index.html.erb new file mode 100644 index 00000000..b7edfcba --- /dev/null +++ b/app/views/api/application_integrations/index.html.erb @@ -0,0 +1,87 @@ +
+
+ +
+
+
+
+ +<% if @applications.any? %> +
+
+

Your applications

+ <% @applications.each do |application| %> + <%= link_to api_application_path(application), class: 'black-text' do %> +
+

+ <%= ApplicationIntegration.icon %> + <%= application.name %> + + live +

+

+ <%= truncate(application.description, length: 400) %> +

+
+
+
+ <%= User.icon %> + <%= pluralize application.integration_authorizations.count, 'user' %> +
+
+ <%= pluralize application.api_requests.successful.count, 'successful request' %> +
+
+
+
+
+ <%= (application.request_error_rate * 100).round(2) %>% error rate + <%= pluralize application.api_requests.errored.count, 'error' %> + +
+
+
+ <% end %> + <% end %> +
+
+

 

+
+
+
Your available features
+
    +
  • + check Basic Notebook.ai endpoints +
  • +
  • + close Premium Notebook.ai endpoints +
  • +
+

+ Some helpful text here +

+
+

+ 47% API quota left +

+
+
+ <%= link_to 'Manage your billing plan', '#' %> +
+
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/api/application_integrations/new.html.erb b/app/views/api/application_integrations/new.html.erb new file mode 100644 index 00000000..8ba8eeff --- /dev/null +++ b/app/views/api/application_integrations/new.html.erb @@ -0,0 +1,5 @@ +

New Application Integration

+ +<%= render 'form', application_integration: @application_integration %> + +<%= link_to 'Back', application_integrations_path %> diff --git a/app/views/api/application_integrations/show.html.erb b/app/views/api/application_integrations/show.html.erb new file mode 100644 index 00000000..5411df57 --- /dev/null +++ b/app/views/api/application_integrations/show.html.erb @@ -0,0 +1,106 @@ +
+ <%= link_to api_applications_path, class: 'grey-text tooltipped', style: 'position: relative; top: 4px;', data: { + position: 'bottom', + enterDelay: '500', + tooltip: "Back to your application list" + } do %> + arrow_back + <% end %> + Your applications +
+ +
+
+
+
+
+ live + + extension + <%= @application_integration.name %> + + (App ID: <%= @application_integration.id %>) +
+
+
+
+ <% [:description].each do |field| %> +
+ <%= field.to_s.humanize %> +
+
+ <%= simple_format @application_integration.send(field) %> +
+
+ <% end %> +
+ Organization +
+
+ <%= link_to @application_integration.organization_name, @application_integration.organization_url %> +
+
+ <% [:website_url, :privacy_policy_url, :authorization_callback_url, :event_ping_url].each do |field| %> +
<%= field.to_s.humanize %>
+
+ <%= link_to @application_integration.send(field), @application_integration.send(field) %> +
+
+ <% end %> +
+
+
+ Application Token +
+
<%= @application_integration.application_token || 'No token set' %>
+
+ Do NOT share this with others you do not trust or make client-side requests that expose this token. This token may be used to make API + calls on your application's behalf and unauthorized use puts you at risk of strangers using up your API quota. +
+
+
+
+
+
+
+
+
+
Billing plan
+ <% if current_user.on_premium_plan? %> +
+ You're on a Premium plan. All endpoints are available. +
+ <% else %> +
+ You are not currently on a Premium plan. API requests that require a Premium plan will succeed for users that have Premium, but fail for other users. + <%= link_to 'Upgrade to a Premium plan', '#' %> to ensure your API requests succeed for all users! +
+ <% end %> +
+
+
API quota
+
+
+ <%= @application_integration.current_quota_usage_percentage %>% used in this billing period +
+
+
+
+

+ You've used <%= number_with_delimiter @application_integration.api_requests.successful.count %> of your alotted 10,000 calls this month. + <%= link_to 'Upgrade to Premium', '#' %> to ensure you don't experience any disruptions when hitting your limit. +

+
+
+
+
Errors
+
+
<%= (@application_integration.request_error_rate * 100).round(2) %>% error rate
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/app/views/integration_authorizations/create.html.erb b/app/views/integration_authorizations/create.html.erb new file mode 100644 index 00000000..03e400a1 --- /dev/null +++ b/app/views/integration_authorizations/create.html.erb @@ -0,0 +1,2 @@ +

IntegrationAuthorizations#create

+

Find me in app/views/integration_authorizations/create.html.erb

diff --git a/app/views/integration_authorizations/show.html.erb b/app/views/integration_authorizations/show.html.erb new file mode 100644 index 00000000..8ace5fd9 --- /dev/null +++ b/app/views/integration_authorizations/show.html.erb @@ -0,0 +1,2 @@ +

IntegrationAuthorizations#show

+

Find me in app/views/integration_authorizations/show.html.erb

diff --git a/app/views/layouts/_common_head.html.erb b/app/views/layouts/_common_head.html.erb index f691c230..655f4a2d 100644 --- a/app/views/layouts/_common_head.html.erb +++ b/app/views/layouts/_common_head.html.erb @@ -10,6 +10,7 @@ +<%# todo: Is there a way to play nicer with thredded's jquery? %> <% unless request.env['REQUEST_PATH'].start_with?('/forum/') %> <% end %> diff --git a/app/views/layouts/_developer_navbar.html.erb b/app/views/layouts/_developer_navbar.html.erb new file mode 100644 index 00000000..16aebd80 --- /dev/null +++ b/app/views/layouts/_developer_navbar.html.erb @@ -0,0 +1,102 @@ +<% if user_signed_in? %> + <%= render partial: 'layouts/navbar/recent_content_dropdown' %> + <%= render partial: 'layouts/modals/search' %> +<% end %> + + diff --git a/app/views/layouts/_seo.html.erb b/app/views/layouts/_seo.html.erb index ec552d07..56de4242 100644 --- a/app/views/layouts/_seo.html.erb +++ b/app/views/layouts/_seo.html.erb @@ -8,7 +8,7 @@ # Default & site-wide values display_meta_tags site: 'Notebook.ai', -publisher: 'https://plus.google.com/118076966717703203223', +publisher: 'https://www.facebook.com/IndentLabs', image_src: image_url('logos/both-original.png'), description: 'Notebook.ai is a set of tools for writers, game designers, and roleplayers to create magnificent universes — and everything within them.', # Recommended keywords tag length: up to 255 characters, 20 words. diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 2b12ae90..5a13289c 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -20,7 +20,6 @@ <%= yield %> - diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f4bf8eab..f6156d86 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -38,6 +38,5 @@ <%= render partial: 'content/keyboard_controls_help_modal' %> <%= render 'layouts/ganalytics' %> - diff --git a/app/views/layouts/developer.html.erb b/app/views/layouts/developer.html.erb new file mode 100644 index 00000000..2b759180 --- /dev/null +++ b/app/views/layouts/developer.html.erb @@ -0,0 +1,34 @@ + + + + <%= render 'layouts/common_head' %> + + + + + <%= render 'layouts/developer_navbar' %> +
+
+
+ Note: The Notebook.ai is currently in private beta and under active development. Not everything has been implemented and tested yet. + Please <%= link_to 'let me know', 'https://github.com/indentlabs/notebook/issues' %> about any problems. +
+
+
+
+
+ <%= yield %> +
+
+
+
+ + <%= react_component("Footer") %> + <%= render 'layouts/ganalytics' %> + + + + \ No newline at end of file diff --git a/app/views/layouts/editor.html.erb b/app/views/layouts/editor.html.erb index 2368dbe9..928522de 100644 --- a/app/views/layouts/editor.html.erb +++ b/app/views/layouts/editor.html.erb @@ -11,7 +11,7 @@
 
- <%# + <%= react_component("PageLookupSidebar", { }) %> diff --git a/app/views/layouts/landing.html.erb b/app/views/layouts/landing.html.erb index c5e5c35a..793272b0 100644 --- a/app/views/layouts/landing.html.erb +++ b/app/views/layouts/landing.html.erb @@ -9,13 +9,9 @@
<%= yield %> - - <%= render 'layouts/ganalytics' %> - -
<%= react_component("Footer") unless defined?(@show_footer) && !@show_footer %> - + <%= render 'layouts/ganalytics' %> diff --git a/config/routes.rb b/config/routes.rb index 4852c61f..2fb4c189 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -248,6 +248,26 @@ Rails.application.routes.draw do # API Endpoints namespace :api do + resources :application_integrations, path: :applications, as: :applications do + get '/authorize', action: :authorize, on: :member + end + + scope '/authorizations' do + post '/create', to: 'integration_authorizations#create', as: :integration_authorizations + end + + get '/', to: 'api_docs#index' + get '/docs', to: 'api_docs#docs' + # get '/applications', to: 'api_docs#applications' + get '/approvals', to: 'api_docs#approvals' + get '/integrations', to: 'api_docs#integrations' + get '/pricing', to: 'api_docs#pricing' + + scope 'docs' do + get '/', to: 'api_docs#index' + get '/references', to: 'api_docs#references' + end + namespace :v1 do scope '/categories' do get '/suggest/:entity_type', to: 'attribute_categories#suggest' @@ -258,6 +278,16 @@ Rails.application.routes.draw do scope '/answers' do get '/suggest/:entity_type/:field_label', to: 'attributes#suggest' end + + # Content index path + Rails.application.config.content_types[:all].each do |content_type| + get "#{content_type.name.downcase.pluralize}", to: "api##{content_type.name.downcase.pluralize}" + end + + # Content show paths + Rails.application.config.content_types[:all].each do |content_type| + get "#{content_type.name.downcase}/:id", to: "api##{content_type.name.downcase}" + end end end diff --git a/db/migrate/20200222051539_create_application_integrations.rb b/db/migrate/20200222051539_create_application_integrations.rb new file mode 100644 index 00000000..26aa3c85 --- /dev/null +++ b/db/migrate/20200222051539_create_application_integrations.rb @@ -0,0 +1,18 @@ +class CreateApplicationIntegrations < ActiveRecord::Migration[6.0] + def change + create_table :application_integrations do |t| + t.references :user, null: false, foreign_key: true + t.string :name + t.string :description + t.string :organization_name + t.string :organization_url + t.string :website_url + t.string :privacy_policy_url + t.string :token + t.datetime :last_used_at + t.string :authorization_callback_url + + t.timestamps + end + end +end diff --git a/db/migrate/20200302223350_add_event_ping_url_to_application_integrations.rb b/db/migrate/20200302223350_add_event_ping_url_to_application_integrations.rb new file mode 100644 index 00000000..312df794 --- /dev/null +++ b/db/migrate/20200302223350_add_event_ping_url_to_application_integrations.rb @@ -0,0 +1,5 @@ +class AddEventPingUrlToApplicationIntegrations < ActiveRecord::Migration[6.0] + def change + add_column :application_integrations, :event_ping_url, :string + end +end diff --git a/db/migrate/20200302224753_add_application_token_to_application_integrations.rb b/db/migrate/20200302224753_add_application_token_to_application_integrations.rb new file mode 100644 index 00000000..a73616f8 --- /dev/null +++ b/db/migrate/20200302224753_add_application_token_to_application_integrations.rb @@ -0,0 +1,5 @@ +class AddApplicationTokenToApplicationIntegrations < ActiveRecord::Migration[6.0] + def change + add_column :application_integrations, :application_token, :string + end +end diff --git a/db/migrate/20200303002751_create_integration_authorizations.rb b/db/migrate/20200303002751_create_integration_authorizations.rb new file mode 100644 index 00000000..b59347d8 --- /dev/null +++ b/db/migrate/20200303002751_create_integration_authorizations.rb @@ -0,0 +1,12 @@ +class CreateIntegrationAuthorizations < ActiveRecord::Migration[6.0] + def change + create_table :integration_authorizations do |t| + t.references :user, null: false, foreign_key: true + t.references :application_integration, null: false, foreign_key: true + t.string :referral_url + t.string :ip_address + + t.timestamps + end + end +end diff --git a/db/migrate/20200911225159_add_authorization_security_log_to_integration_authorizations.rb b/db/migrate/20200911225159_add_authorization_security_log_to_integration_authorizations.rb new file mode 100644 index 00000000..bc39f562 --- /dev/null +++ b/db/migrate/20200911225159_add_authorization_security_log_to_integration_authorizations.rb @@ -0,0 +1,7 @@ +class AddAuthorizationSecurityLogToIntegrationAuthorizations < ActiveRecord::Migration[6.0] + def change + add_column :integration_authorizations, :origin, :string + add_column :integration_authorizations, :content_type, :string + add_column :integration_authorizations, :user_agent, :string + end +end diff --git a/db/migrate/20200911231223_add_user_token_to_integration_authorizations.rb b/db/migrate/20200911231223_add_user_token_to_integration_authorizations.rb new file mode 100644 index 00000000..6ca89ec7 --- /dev/null +++ b/db/migrate/20200911231223_add_user_token_to_integration_authorizations.rb @@ -0,0 +1,5 @@ +class AddUserTokenToIntegrationAuthorizations < ActiveRecord::Migration[6.0] + def change + add_column :integration_authorizations, :user_token, :string + end +end diff --git a/db/migrate/20200912000306_create_api_requests.rb b/db/migrate/20200912000306_create_api_requests.rb new file mode 100644 index 00000000..0be2901f --- /dev/null +++ b/db/migrate/20200912000306_create_api_requests.rb @@ -0,0 +1,13 @@ +class CreateApiRequests < ActiveRecord::Migration[6.0] + def change + create_table :api_requests do |t| + t.references :application_integration, null: true, foreign_key: true + t.references :integration_authorization, null: true, foreign_key: true + t.string :result + t.integer :updates_used, default: 0 + t.string :ip_address + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f4ff1ade..4e67e44b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_07_22_004641) do +ActiveRecord::Schema.define(version: 2020_09_12_000306) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false @@ -41,6 +41,36 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.index ["user_id"], name: "index_api_keys_on_user_id" end + create_table "api_requests", force: :cascade do |t| + t.integer "application_integration_id" + t.integer "integration_authorization_id" + t.string "result" + t.integer "updates_used", default: 0 + t.string "ip_address" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["application_integration_id"], name: "index_api_requests_on_application_integration_id" + t.index ["integration_authorization_id"], name: "index_api_requests_on_integration_authorization_id" + end + + create_table "application_integrations", force: :cascade do |t| + t.integer "user_id", null: false + t.string "name" + t.string "description" + t.string "organization_name" + t.string "organization_url" + t.string "website_url" + t.string "privacy_policy_url" + t.string "token" + t.datetime "last_used_at" + t.string "authorization_callback_url" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "event_ping_url" + t.string "application_token" + t.index ["user_id"], name: "index_application_integrations_on_user_id" + end + create_table "archenemyships", force: :cascade do |t| t.integer "user_id" t.integer "character_id" @@ -1421,6 +1451,21 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.index ["user_id"], name: "index_image_uploads_on_user_id" end + create_table "integration_authorizations", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "application_integration_id", null: false + t.string "referral_url" + t.string "ip_address" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "origin" + t.string "content_type" + t.string "user_agent" + t.string "user_token" + t.index ["application_integration_id"], name: "index_integration_authorizations_on_application_integration_id" + t.index ["user_id"], name: "index_integration_authorizations_on_user_id" + end + create_table "item_magics", force: :cascade do |t| t.integer "item_id" t.integer "magic_id" @@ -2168,6 +2213,7 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.datetime "updated_at", precision: 6, null: false t.string "explanation" t.string "cached_content_name" + t.datetime "deleted_at" t.index ["content_type", "content_id"], name: "polycontent_collection_index" t.index ["page_collection_id"], name: "index_page_collection_submissions_on_page_collection_id" t.index ["user_id"], name: "index_page_collection_submissions_on_user_id" @@ -2187,6 +2233,7 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.string "description" t.boolean "allow_submissions" t.string "slug" + t.datetime "deleted_at" t.index ["user_id"], name: "index_page_collections_on_user_id" end @@ -2243,6 +2290,7 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.integer "page_unlock_promo_code_id" t.string "approval_url" t.string "payer_id" + t.datetime "deleted_at" t.index ["page_unlock_promo_code_id"], name: "index_paypal_invoices_on_page_unlock_promo_code_id" t.index ["user_id"], name: "index_paypal_invoices_on_user_id" end @@ -3111,6 +3159,7 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do t.integer "position" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.datetime "deleted_at" t.index ["timeline_id"], name: "index_timeline_events_on_timeline_id" end @@ -3402,6 +3451,9 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "api_keys", "users" + add_foreign_key "api_requests", "application_integrations" + add_foreign_key "api_requests", "integration_authorizations" + add_foreign_key "application_integrations", "users" add_foreign_key "buildings", "universes" add_foreign_key "buildings", "users" add_foreign_key "character_birthtowns", "characters" @@ -3561,6 +3613,8 @@ ActiveRecord::Schema.define(version: 2020_07_22_004641) do add_foreign_key "group_creatures", "groups" add_foreign_key "group_creatures", "users" add_foreign_key "image_uploads", "users" + add_foreign_key "integration_authorizations", "application_integrations" + add_foreign_key "integration_authorizations", "users" add_foreign_key "item_magics", "items" add_foreign_key "item_magics", "magics" add_foreign_key "item_magics", "users" diff --git a/lib/tasks/data_migrations.rake b/lib/tasks/data_migrations.rake index f402b1e2..180800c9 100644 --- a/lib/tasks/data_migrations.rake +++ b/lib/tasks/data_migrations.rake @@ -64,6 +64,11 @@ namespace :data_migrations do end end + desc "Add developer billing plans" + task create_developer_billing_plans: :environment do + # TODO + end + desc "Add bandwidth bonuses to billing plans" task billing_plan_bandwidths: :environment do puts "Updating bandwidths for all billing plans" diff --git a/test/controllers/api_docs_controller_test.rb b/test/controllers/api_docs_controller_test.rb new file mode 100644 index 00000000..d871c9b2 --- /dev/null +++ b/test/controllers/api_docs_controller_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class ApiDocsControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get api_docs_index_url + assert_response :success + end + +end diff --git a/test/controllers/application_integrations_controller_test.rb b/test/controllers/application_integrations_controller_test.rb new file mode 100644 index 00000000..d4c1498d --- /dev/null +++ b/test/controllers/application_integrations_controller_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class ApplicationIntegrationsControllerTest < ActionDispatch::IntegrationTest + setup do + @application_integration = application_integrations(:one) + end + + test "should get index" do + get application_integrations_url + assert_response :success + end + + test "should get new" do + get new_application_integration_url + assert_response :success + end + + test "should create application_integration" do + assert_difference('ApplicationIntegration.count') do + post application_integrations_url, params: { application_integration: { } } + end + + assert_redirected_to application_integration_url(ApplicationIntegration.last) + end + + test "should show application_integration" do + get application_integration_url(@application_integration) + assert_response :success + end + + test "should get edit" do + get edit_application_integration_url(@application_integration) + assert_response :success + end + + test "should update application_integration" do + patch application_integration_url(@application_integration), params: { application_integration: { } } + assert_redirected_to application_integration_url(@application_integration) + end + + test "should destroy application_integration" do + assert_difference('ApplicationIntegration.count', -1) do + delete application_integration_url(@application_integration) + end + + assert_redirected_to application_integrations_url + end +end diff --git a/test/controllers/integration_authorizations_controller_test.rb b/test/controllers/integration_authorizations_controller_test.rb new file mode 100644 index 00000000..869870d5 --- /dev/null +++ b/test/controllers/integration_authorizations_controller_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +class IntegrationAuthorizationsControllerTest < ActionDispatch::IntegrationTest + test "should get create" do + get integration_authorizations_create_url + assert_response :success + end + + test "should get show" do + get integration_authorizations_show_url + assert_response :success + end + +end diff --git a/test/fixtures/api_requests.yml b/test/fixtures/api_requests.yml new file mode 100644 index 00000000..5c3cc8e0 --- /dev/null +++ b/test/fixtures/api_requests.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + application_integration: one + integration_authorization: one + result: MyString + +two: + application_integration: two + integration_authorization: two + result: MyString diff --git a/test/fixtures/application_integrations.yml b/test/fixtures/application_integrations.yml new file mode 100644 index 00000000..a5bcf898 --- /dev/null +++ b/test/fixtures/application_integrations.yml @@ -0,0 +1,25 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + name: MyString + description: MyString + organization_name: MyString + organization_url: MyString + website_url: MyString + privacy_policy_url: MyString + token: MyString + last_used_at: 2020-02-21 23:15:39 + authorization_callback_url: MyString + +two: + user: two + name: MyString + description: MyString + organization_name: MyString + organization_url: MyString + website_url: MyString + privacy_policy_url: MyString + token: MyString + last_used_at: 2020-02-21 23:15:39 + authorization_callback_url: MyString diff --git a/test/fixtures/integration_authorizations.yml b/test/fixtures/integration_authorizations.yml new file mode 100644 index 00000000..aa25bea1 --- /dev/null +++ b/test/fixtures/integration_authorizations.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + application_integration: one + referral_url: MyString + ip_address: MyString + +two: + user: two + application_integration: two + referral_url: MyString + ip_address: MyString diff --git a/test/models/api_request_test.rb b/test/models/api_request_test.rb new file mode 100644 index 00000000..ea37ce4c --- /dev/null +++ b/test/models/api_request_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ApiRequestTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/application_integration_test.rb b/test/models/application_integration_test.rb new file mode 100644 index 00000000..4dd205d9 --- /dev/null +++ b/test/models/application_integration_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ApplicationIntegrationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/integration_authorization_test.rb b/test/models/integration_authorization_test.rb new file mode 100644 index 00000000..a8fc2082 --- /dev/null +++ b/test/models/integration_authorization_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class IntegrationAuthorizationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/application_integrations_test.rb b/test/system/application_integrations_test.rb new file mode 100644 index 00000000..62f44e76 --- /dev/null +++ b/test/system/application_integrations_test.rb @@ -0,0 +1,41 @@ +require "application_system_test_case" + +class ApplicationIntegrationsTest < ApplicationSystemTestCase + setup do + @application_integration = application_integrations(:one) + end + + test "visiting the index" do + visit application_integrations_url + assert_selector "h1", text: "Application Integrations" + end + + test "creating a Application integration" do + visit application_integrations_url + click_on "New Application Integration" + + click_on "Create Application integration" + + assert_text "Application integration was successfully created" + click_on "Back" + end + + test "updating a Application integration" do + visit application_integrations_url + click_on "Edit", match: :first + + click_on "Update Application integration" + + assert_text "Application integration was successfully updated" + click_on "Back" + end + + test "destroying a Application integration" do + visit application_integrations_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Application integration was successfully destroyed" + end +end